DEV Community

Andi
Andi

Posted on

Go (Golang) Basic - Pointers

One of Go's Most Feared (and Powerful) Concepts

Welcome back! In our journey so far, we've learned how to create our own data types with structs and attach behavior with methods. Now, we're going to tackle a topic that can seem intimidating at first but is absolutely essential for writing effective Go code: Pointers.

Don't worry. We'll break it down with a simple analogy that makes it easy to understand. The core idea is to understand the difference between giving someone a copy of your data versus giving them access to the original.

1. Go's Default Behavior: Pass-by-Value (The Photocopy)

By default, whenever you pass data to a function in Go (including structs), Go makes a copy of that data. The function then works on the copy, leaving the original untouched.

Analogy: You have an important document. If you give a photocopy to a colleague and they scribble all over it, your original document remains clean and unchanged.

A Code Example That Doesn't Work as Expected

Let's try to write a method that updates a user's email.

package main

import "fmt"

type User struct {
    Name  string
    Email string
}

// This method receives a COPY of the User
func (u User) updateEmail(newEmail string) {
    u.Email = newEmail
    fmt.Println("Inside method, email is:", u.Email)
}

func main() {
    // Create a user
    user := User{Name: "Budi", Email: "budi.lama@email.com"}
    fmt.Println("Before method call, email is:", user.Email)

    // We call the method to update the email
    user.updateEmail("budi.baru@email.com")

    // Let's check the original user's email again
    fmt.Println("After method call, email is:", user.Email)
}
Enter fullscreen mode Exit fullscreen mode

When you run this, you'll see this output:

Before method call, email is: budi.lama@email.com
Inside method, email is: budi.baru@email.com
After method call, email is: budi.lama@email.com
Enter fullscreen mode Exit fullscreen mode

Notice that the original user's email did not change! This is because the updateEmail method worked on a photocopy, not the original.

So, how do we modify the original? With pointers.

2. The Solution: Pass-by-Reference with Pointers

To solve our problem, we need to stop sending a photocopy and instead send the address of the original document. In Go, this "address" is called a pointer.

A pointer doesn't hold the data itself, but it holds the memory location where the data lives. When a function receives a pointer, it knows where to find the original data and can modify it directly.

We use two special symbols for this:

  • * (The Star/Asterisk): When used in a type declaration like *User, it means "a pointer to a User".
  • & (The Ampersand): When used in front of a variable like &user, it means "get the memory address of this user".

3. The Most Important Use Case: Pointer Receivers

The most common and important place you'll use pointers is on method receivers. By adding a * to the receiver, you tell Go that this method should work on the original struct instance, not a copy.

The Corrected Code Example

Let's fix our previous example by adding one character: a *.

package main

import "fmt"

type User struct {
    Name  string
    Email string
}

// This method now receives a POINTER to the User (*User)
func (u *User) updateEmail(newEmail string) {
    u.Email = newEmail // This now modifies the original user's email
}

func main() {
    // Create a user
    user := User{Name: "Budi", Email: "budi.lama@email.com"}
    fmt.Println("Before method call, email is:", user.Email)

    // When you call a method with a pointer receiver on a variable,
    // Go automatically handles sending the address for you (it's like implicitly doing `(&user).updateEmail(...)`)
    user.updateEmail("budi.baru@email.com")

    // Let's check the original user's email again
    fmt.Println("After method call, email is:", user.Email)
}
Enter fullscreen mode Exit fullscreen mode

Now, the output is exactly what we want:

Before method call, email is: budi.lama@email.com
After method call, email is: budi.baru@email.com
Enter fullscreen mode Exit fullscreen mode

Success! The original user struct was modified because the method received a pointer to it.

Why use pointer receivers?

  1. To Modify Data: As we just saw, it's the only way for a method to change the original struct's values.
  2. For Performance: It avoids copying large structs every time a method is called, making your program faster and more memory-efficient.

Conclusion

Pointers might seem complex, but their main purpose is simple: to give functions and methods direct access to modify original data.

Here's a golden rule for you as a beginner:

When you write methods for a struct, always use a pointer receiver (func (s *MyStruct) ...).

This is the standard practice in Go and will save you from many common bugs and performance issues.

In the next part, we'll explore another powerful concept for writing flexible code: Interfaces. See you there!

Top comments (0)