Go Pointers, Methods and Value Semantics
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Go has pointers but no pointer arithmetic — you cannot walk memory with p++. There is exactly one purpose: deciding whether a value is copied or shared.
Address-of and dereference
x := 1
p := &x // p has type *int
fmt.Println(*p) // 1
*p = 2 // write through the pointer
fmt.Println(x) // 2
The zero value of every pointer type is nil:
var p *int
fmt.Println(p == nil) // true
fmt.Println(*p) // panic: nil pointer dereference
new(T) allocates a zeroed T and returns a pointer — equivalent to declaring a variable and taking its address:
p := new(int) // *int pointing at 0
In daily code composite literals are more common because they set fields at the same time:
type User struct{ Name string }
u := &User{Name: "ann"}
There are no constructors in Go; a NewUser(...) function returning *User is a convention, not a language feature.
Pointers as parameters
func increment(n *int) {
*n++
}
x := 1
increment(&x)
fmt.Println(x) // 2
Passing a value copies it; passing a pointer lets the function reach the caller’s variable. But returning a new value is usually the better design:
func increment(n int) int {
return n + 1
}
Mutating a parameter makes a function stateful and harder to test. Reserve out-parameters for cases with an explicit “update in place” meaning — json.Unmarshal(data, &cfg), rows.Scan(&name, &age) — where the API cannot return the value any other way.
Value versus pointer receivers
type Counter struct {
n int
}
func (c Counter) IncByValue() {
c.n++ // mutates the copy: no effect
}
func (c *Counter) Inc() {
c.n++ // mutates the original
}
c := Counter{}
c.Inc()
c.IncByValue()
fmt.Println(c.n) // 1
The call c.Inc() is shorthand for (&c).Inc() — Go takes the address automatically, as long as c is addressable. Variables, struct fields and slice elements are; map elements and function results are not:
counters := map[string]Counter{"a": {}}
counters["a"].Inc() // compile error: cannot call pointer method on map index
c := counters["a"] // copy out, mutate, write back
c.Inc()
counters["a"] = c
Storing map[string]*Counter avoids the dance entirely.
Method sets and interfaces
The rule that bites during interface work: the method set of *T contains methods with both value and pointer receivers; the method set of T contains only value receivers.
type Incrementer interface {
Inc()
}
var _ Incrementer = &Counter{} // ok: *Counter has Inc
var _ Incrementer = Counter{} // compile error: Counter has no Inc
So when a type implements an interface through pointer receivers, you must put &T{} into the interface, not T{}. The reverse (value receivers, pointer in the interface) is always fine.
Practical guidance: pick one receiver kind per type and stick to it. Mixing produces APIs where some methods change state and others silently do not.
nil receivers
A method can handle a nil receiver, which is a legitimate technique for linked structures:
type Node struct {
Val int
Next *Node
}
func (n *Node) Len() int {
if n == nil { // calling a method on nil is allowed
return 0
}
return 1 + n.Next.Len()
}
What is never allowed is reading a field through a nil pointer — that panics, so the nil check has to come first. And a nil pointer stored inside an interface does not make the interface nil, which is the trap in the final chapter.
When to use a pointer
| Situation | Pointer? |
|---|---|
| The method must modify the receiver or parameter | ✅ required |
| The struct is large (many fields, embedded arrays) | ✅ avoids copying |
| You need to express “may be absent” | ✅ nil is meaningful |
| Other methods on the type use pointer receivers | ✅ stay consistent |
| Small struct you only read from | ❌ value is simpler |
slice, map, channel, func |
❌ already reference-like |
The last row is the one people over-correct: []byte does not copy its backing array when passed, so *[]byte adds indirection and a nil case for nothing.
Escape analysis: not your problem
When a local variable’s address outlives the function, the compiler moves it to the heap automatically:
func NewUser(name string) *User {
return &User{Name: name} // legal; Go allocates on the heap
}
This is not the C mistake of returning a pointer to a dead stack frame — the compiler proves the escape and handles it. If you care about allocation counts, inspect the decision rather than guessing:
go build -gcflags="-m" ./...
Optimise only after a benchmark shows the allocation matters; readability first.
Common mistakes
1. Dereferencing nil
*p or p.Field panics. Check p != nil before use.
2. Mutating fields in a value receiver
The method silently does nothing useful. Switch to a pointer receiver.
3. Assigning a value to an interface implemented with pointer receivers
var _ I = T{} is a compile error. Pass &T{}.
4. Calling pointer methods on map elements
Not addressable. Copy out, mutate, write back.
5. Turning every parameter into a pointer
Small structs are cheaper and safer by value, with no nil case to handle.
6. Using pointers as a mutability switch
Idiomatic Go returns new values; pointers are for explicit in-place updates and identity.
Summary
&takes an address,*dereferences; there is no pointer arithmetic.- Values copy, pointers share; prefer returning results over out-parameters.
- Pointer receivers can mutate, value receivers cannot; method sets decide interface satisfaction.
- Automatic
&requires an addressable value — map elements are not. - Pointers for mutation, large structs and optionality; slices and maps already behave like references.
Next: interfaces and composition — implicit satisfaction, small interfaces, and the nil-interface trap.