Skip to main content

Go from scratch, part 10: pointers and when to use them (no C flashbacks)

Curso: Go-en — Lección 10 de 15

> series: learn go from 0 to 100 | article 10 of 19

The word pointer gives cold sweats to anyone who touched C in college, and puzzlement to anyone coming from Python or JS, where everything is a reference and nobody says so. Go pointers are the domesticated version: the power to choose, minus the arithmetic and the recreational segfaults.


The mechanics: & and *

Two operators, that’s it (A Tour of Go, pointers and spec):

x := 42
p := &x // & takes the address: p is *int
fmt.Println(*p) // * dereferences: 42

*p = 21 // modifies x through p
fmt.Println(x) // 21

// What's NOT here: pointer arithmetic.
// p++ won't compile. No strolling through memory.

A pointer’s zero value is nil, and dereferencing nil is an instant panic. Unlike C, there’s no pointer arithmetic (FAQ): the garbage collector has your back and the compiler kills the ninja urges.

Everything is passed by value. Everything.

Here’s the key that sorts out Go pointers once and for all: in Go EVERYTHING is passed as a copy. The question is what gets copied. With a struct, the whole struct. With a pointer, the address (8 bytes), and both copies look at the same place:

type User struct {
  Name string
  Visits int
}

// Copy: the caller never sees the change.
func visitWrong(u User) {
  u.Visits++
}

// Pointer: mutates the original.
func visit(u *User) {
  u.Visits++ // Go auto-dereferences: no (*u) needed
}

u := User{Name: "Jenn"}
visit(&u)
fmt.Println(u.Visits) // 1

Mental bonus for article 8: slices and maps look pass-by-reference because their header contains an internal pointer. The header gets copied, not the data. It all clicks now, right?

Go pointers: Pixelart diagram illustrating Go pointers: pass by value with two independent boxes, versus pass by pointer with one shared arrow to a single box
[Value copies, pointer shares: the decision that changes everything.]

When to use a pointer (and when not to)

The cheat sheet, distilled from community conventions and Effective Go:

  • Pointer when the function or method must MUTATE the receiver or argument.
  • Pointer when the struct is big and copying it per call hurts (measure before optimizing, though).
  • Pointer when you need to tell “no value” (nil) apart from “zero value”.
  • Value for small immutable types: numbers, strings, small read-only structs.
  • Consistency: if one method of a type takes a pointer receiver, they all should.

And a liberation for former C programmers:

// Two ways to get a pointer to a fresh struct:
p1 := new(User) // zero values
p2 := &User{ // literal, the most common
  Name: "Ada",
}

// Returning pointers to locals is SAFE:
// the compiler moves them to the heap if needed.
func create() *User {
  u := User{Name: "Grace"}
  return &u // in C this was a sin. Not here.
}

Returning a local variable’s address is idiomatic and safe: the compiler’s escape analysis decides whether it lives on the stack or moves to the heap (Effective Go, allocation). You write clear code, it handles the memory.


Weekly challenge: put Go pointers to work

  • Write swap(a, b *int) crossing two variables’ values and verify from main.
  • Revisit article 5‘s Account and verify that Withdraw with a value receiver deducts nothing, and with a pointer receiver it does.
  • Create func find(id int) *User returning nil when not found, and handle that nil at the call site without panicking.
  • Bonus: print &x in two separate runs and watch the address change. Addresses are runtime details, not data.

Go pointers: level complete.

Next article the main course begins: goroutines and the CSP model. The concurrency you came to Go for.

> exit
Retrato pixel art de Jenniffer Cubillos

thanks for reading