DEV Community

Cover image for Pointers in Go: A Practical Guide for Developers
Benjamin Koimett
Benjamin Koimett

Posted on

Pointers in Go: A Practical Guide for Developers

Understanding the Basics

Pointers in Go are often misunderstood by developers coming from languages that hide pointer complexity or don't have them at all. At their core, pointers are simply variables that store memory addresses rather than actual values.


var x int = 42
var ptr *int = &x  // & gets the address, *int declares a pointer type
Enter fullscreen mode Exit fullscreen mode

Why Pointers Matter in Go

1. Performance and Memory Efficiency

When you pass large structs to functions, Go copies the entire struct by default. Pointers let you pass a reference instead:


type User struct {
    ID   int
    Name string
    // ... 20 more fields
}

// Inefficient - copies entire struct
func processUser(u User) {
    // ...
}

// Efficient - passes reference
func processUserPtr(u *User) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

2. Modifying Function Arguments

Go passes arguments by value, meaning functions receive copies. To modify the original, you need pointers:


func increment(x int) {
    x++ // Only modifies the copy
}

func incrementPtr(x *int) {
    *x++ // Dereferences and modifies the original
}

func main() {
    val := 10
    increment(val)     // val is still 10
    incrementPtr(&val) // val becomes 11
}
Enter fullscreen mode Exit fullscreen mode

3. Working with Methods

Go has both value and pointer receivers for methods, which affects mutability:


type Counter struct {
    value int
}

// Value receiver - works on a copy
func (c Counter) Get() int {
    return c.value
}

// Pointer receiver - can modify original
func (c *Counter) Increment() {
    c.value++
}

func main() {
    c := Counter{value: 5}
    c.Increment() // Go automatically converts to (&c).Increment()
    fmt.Println(c.Get()) // Output: 6
}
Enter fullscreen mode Exit fullscreen mode

Common Pointer Patterns

The nil Pointer Check

Always check for nil pointers before dereferencing:
go

func printName(user *User) {
    if user == nil {
        fmt.Println("User is nil")
        return
    }
    fmt.Println(user.Name)
}
Enter fullscreen mode Exit fullscreen mode

Pointer to Pointer (Rare but Useful)

Sometimes you need multiple levels of indirection:
go

func allocateMatrix(rows, cols int) **int {
    matrix := make([]*int, rows)
    for i := range matrix {
        row := make([]int, cols)
        matrix[i] = &row[0]
    }
    return &matrix[0]
}
Enter fullscreen mode Exit fullscreen mode

Returning Pointers from Functions

Returning pointers to local variables is safe in Go thanks to escape analysis:
go

func createUser(name string) *User {
    return &User{Name: name} // Go allocates this on the heap
}
Enter fullscreen mode Exit fullscreen mode

When to Use Pointers (and When Not To)

Use pointers when:

  • Structs are large (prevents copying)
  • You need to modify the original value
  • Working with methods that change receiver state
  • Dealing with optional parameters (nil pointer = not provided)

Avoid pointers when:

  • Working with basic types (int, bool, string) unless modification is needed
  • The struct is small (copying is cheaper than dereferencing)
  • You want immutability by default
  • Working with concurrency (pointers require careful synchronization)

Gotchas and Best Practices

1. Pointer Arithmetic is Not Allowed

Unlike C, Go doesn't allow pointer arithmetic for safety:
go

// This won't compile in Go
ptr := &arr[0]
ptr++ // Error: invalid operation
Enter fullscreen mode Exit fullscreen mode

2. Maps and Slices are Already References

Maps and slices already contain pointers internally, so you often don't need pointers to them:
go


func modifySlice(s []int) {
    s[0] = 100 // Modifies the original slice
}
Enter fullscreen mode Exit fullscreen mode

3. Interface Values Can Hold Pointers

When storing structs in interfaces, be aware of the pointer/value distinction:
go

type Speaker interface {
    Speak() string
}

type Dog struct{ name string }

func (d *Dog) Speak() string { return "Woof!" }

func main() {
    var s Speaker
    d := Dog{name: "Rex"}
    // s = d     // Error: Dog doesn't implement Speaker
    s = &d       // OK: *Dog implements Speaker
}
Enter fullscreen mode Exit fullscreen mode

4. Use new() vs. Composite Literals

// Both are valid, but have different use cases
u1 := new(User)     // Zero-valued User, returns *User
u2 := &User{ID: 1}  // Initialized User, returns *User
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Go's compiler performs escape analysis to determine whether variables should be allocated on the heap or stack. Don't prematurely optimize by using pointers everywhere - let the compiler do its job and profile your code first.

// The compiler will determine where to allocate
func createUser() *User {
    u := User{Name: "Alice"} // Might be stack or heap allocated
    return &u
}
Enter fullscreen mode Exit fullscreen mode

Testing with Pointers

When testing functions that use pointers, remember that &User{} creates a pointer:
The key to mastering pointers is practice. Start with simple cases, understand when data is being copied versus referenced, and gradually work up to more complex patterns. Happy coding!

func TestProcessUser(t *testing.T) {
    user := &User{Name: "Test"}
    result := processUserPtr(user)
    // Test assertions
}
Enter fullscreen mode Exit fullscreen mode

## Conclusion

Pointers in Go strike a balance between C's explicit memory control and higher-level language safety. They're essential for:

  • Writing efficient code with large data structures
  • Implementing mutable method receivers
  • Creating flexible APIs with optional parameters

Remember the golden rule: Use pointers when you need to share or modify data, otherwise prefer values. This approach gives you performance when needed while maintaining code clarity and safety.

The key to mastering pointers is practice. Start with simple cases, understand when data is being copied versus referenced, and gradually work up to more complex patterns. Happy coding!

Top comments (0)