Skip to content
GoBeginner6 min read

Pointers in Go: When to Use *T and Why

What a pointer actually is, why Go passes everything by value by default, and the concrete rule for when a function or method should take a pointer instead of a copy — with diagrams.

GoBackend.dev
GoPointersMemory

TL;DR

Go passes everything by value by default, so a function only sees a copy of its arguments unless you pass a pointer — a value holding the address of another value, letting the function reach back and modify the original.

What You'll Learn

  • What a pointer actually stores, and what & and * do
  • Why Go functions can't mutate their arguments unless a pointer is passed
  • The concrete rule for choosing a pointer receiver vs. a value receiver on a method
  • Why passing a large struct by pointer avoids a copy, and when that copy doesn't actually matter
  • What a nil pointer is, and what happens when you dereference one
  • Why Go has no pointer arithmetic, unlike C

The problem

func double(n int) {
	n = n * 2
}
 
func main() {
	x := 5
	double(x)
	fmt.Println(x) // 5 — not 10
}

double runs, multiplies something by two, and yet x is unchanged. This isn't a bug — it's Go's most consistent rule: every function call copies its arguments. n inside double is a brand-new int, initialized to a copy of x's value; multiplying it does nothing to the original. Pointers are how you opt out of that copy when you actually need to.

What a pointer is

A pointer is a value that holds the memory address of another value — nothing more exotic than that. Two operators do all the work:

  • &x — "give me the address of x" — produces a pointer to x
  • *p — "the value at the address p points to" — dereferences the pointer, either to read it or to assign through it
x := 5
p := &x        // p is a *int, holding x's address
fmt.Println(*p) // 5 — dereference to read the value
*p = 10        // dereference to write through the pointer
fmt.Println(x)  // 10 — x itself changed

Rendering diagram…

Fixing double with a pointer

func double(n *int) {
	*n = *n * 2 // dereference, multiply, write back through the pointer
}
 
func main() {
	x := 5
	double(&x) // pass the address of x
	fmt.Println(x) // 10
}

double still receives a copy — but this time it's a copy of the address, not the value. Both the caller's x and the copy inside double point at the exact same memory, so writing through *n is visible to the caller the instant the function runs.

Pointer receivers vs. value receivers

This same idea decides whether a method should take a pointer or a value receiver:

type Counter struct {
	count int
}
 
func (c Counter) IncrementCopy() { // value receiver
	c.count++ // mutates the copy, not the original
}
 
func (c *Counter) Increment() { // pointer receiver
	c.count++ // mutates through the pointer — the original changes
}
c := Counter{}
c.IncrementCopy()
fmt.Println(c.count) // 0 — the method only ever saw a copy
 
c.Increment()
fmt.Println(c.count) // 1 — the method mutated the real thing

(Note c.Increment() works even though c isn't a pointer — Go automatically takes &c for you when calling a pointer-receiver method on an addressable value. You don't need to write (&c).Increment() yourself.)

The rule that covers almost every case:

  • Use a pointer receiver when the method needs to mutate the receiver, or when the receiver is a large struct where copying it on every call would be wasteful.
  • Use a value receiver when the type is small (an int, a small struct, a slice/map — which are already cheap header copies) and the method has no reason to modify the caller's copy.

Mixing both on the same type is legal but confusing — if any method on a type needs a pointer receiver, it's conventional to make all of that type's methods pointer receivers, so callers don't have to remember which methods mutate and which don't.

Why this matters for large structs

Passing a struct by value copies every field, every time:

type Config struct {
	Name    string
	Options [50]int // a sizable array field
}
 
func process(c Config) { /* ... */ }       // copies the whole struct
func processFast(c *Config) { /* ... */ }  // copies one pointer (8 bytes)

For a small struct, the copy is cheap enough that this doesn't matter — don't reach for a pointer purely for performance until profiling says a copy is actually the bottleneck. For a large one, or one that's copied millions of times in a hot path, a pointer avoids the repeated copy of every field.

nil pointers

A pointer's zero value is nil — it holds no address at all. Dereferencing one is one of the most common panics in Go:

var p *int   // nil — no address
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference

This is also why a function returning *SomeType, error should always be checked for an error before touching the returned pointer — a failed lookup often returns (nil, err), and dereferencing that nil before checking err is a direct route to this panic.

No pointer arithmetic

Coming from C, the next question is usually "can I do p++ to walk through memory?" — no. Go pointers support exactly two operations: dereference (*p) and comparison (p == nil, p1 == p2). There's no way to move a pointer to point somewhere else by arithmetic, which is a deliberate safety choice: it's what makes it impossible for a Go pointer to wander off into memory it was never given a legitimate address to.

Common mistakes

  • Expecting a plain value parameter to let a function mutate the caller's variable. It can't — pass a pointer if mutation is the goal.
  • Dereferencing a pointer before checking whether it's nil, especially one returned alongside an error.
  • Reaching for a pointer receiver "for performance" on a small struct without measuring. The copy is usually irrelevant; the mutation semantics are the real reason to choose one.
  • Mixing pointer and value receivers on the same type without a reason — pick one convention for that type and stay consistent.

Summary

Go copies every function argument by default; a pointer is how you pass an address instead of a value, letting a function or method reach back and modify something the caller can see. Choose a pointer receiver when a method needs to mutate its receiver or the receiver is large; choose a value receiver otherwise. A nil pointer holds no address, and dereferencing one panics — check errors before touching a pointer a function returned alongside one. With no pointer arithmetic, that's the entire, memory-safe surface area pointers have in Go.

Key Takeaways

  • Every Go function call copies its arguments; a pointer parameter copies an address, letting the function reach back and modify the original value
  • Use a pointer receiver when a method needs to mutate the receiver, or when the value is large enough that copying it is wasteful
  • Use a value receiver when the type is small (an int, a small struct) and the method shouldn't be able to mutate the caller's copy
  • A nil pointer holds no address; dereferencing it (*p) panics with a nil pointer dereference
  • Go has no pointer arithmetic — a pointer can only be dereferenced or compared, never incremented, which is what keeps it memory-safe