Go by Example

Pointers

The & and * operators, pointer to struct, nil pointer, and the pointer vs value semantics decision.

A pointer holds the memory address of a value. Go has pointers but no pointer arithmetic - you cannot increment a pointer to step through memory the way C allows. This keeps pointers useful (sharing, avoiding copies, mutating through function calls) without most of the footguns.

& takes the address of a value and produces a pointer. * dereferences a pointer - it reads the value at that address. Assigning through a dereferenced pointer mutates the original.

package main
 
import "fmt"
 
func increment(n *int) {
    *n++ // dereference and mutate
}
 
func main() {
    x := 42
    p := &x // p is *int, holds address of x
 
    fmt.Println(x, *p) // 42 42 - same value, two ways to reach it
 
    *p = 100
    fmt.Println(x) // 100 - x was mutated through p
 
    increment(&x)
    fmt.Println(x) // 101
}

Struct fields are accessed through a pointer with the same . syntax as value access - Go automatically dereferences the pointer. The new built-in allocates a zeroed value and returns a pointer to it.

package main
 
import "fmt"
 
type Point struct {
    X, Y int
}
 
func main() {
    // Pointer to struct via & on a literal
    p := &Point{X: 1, Y: 2}
 
    // Field access through pointer - no explicit dereference needed
    fmt.Println(p.X, p.Y) // 1 2
    p.X = 10
    fmt.Println(p.X) // 10
 
    // new: allocates a zeroed Point and returns *Point
    q := new(Point)
    fmt.Println(q.X, q.Y) // 0 0
}

The zero value of a pointer is nil. Dereferencing a nil pointer causes a panic at runtime. Always check before dereferencing a pointer that may be nil.

package main
 
import "fmt"
 
func describe(p *Point) string {
    if p == nil {
        return "<nil>"
    }
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
 
type Point struct{ X, Y int }
 
func main() {
    var p *Point // nil
    fmt.Println(describe(p))            // <nil>
    fmt.Println(describe(&Point{3, 4})) // (3, 4)
}

In production

If any method on a type needs to mutate the receiver, all methods on that type should use pointer receivers - mixing pointer and value receivers on the same named type breaks the method set and can cause a type to fail to satisfy an interface in non-obvious ways. The rule of thumb: use a pointer receiver if the struct is large (avoids copies on every call), if any method needs to mutate state, or if the zero value of the type is not meaningful. Use a value receiver only when the struct is small and all methods are read-only. Consistency within a type is more important than the size heuristic.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free