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 1.24

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.