CHAPTER 16 · COMPOSITE TYPES

Pointers

A pointer is a variable whose value is the memory address of another variable. Go has pointers, but makes them easy and safe: no arithmetic, no manual allocation (the garbage collector handles that), and automatic dereferencing through struct fields. You'll use them mostly to share state across function boundaries.

Learning objectives

  • Explain what a pointer stores.
  • Use & to take an address and * to dereference.
  • Understand why Go is pass-by-value, and how pointers give you indirection when you need it.
  • Know when to prefer a pointer vs a value.
  • Check for nil before dereferencing.

What's a pointer?

x := 42
p := &x            // p is a *int pointing to x
fmt.Println(p)      // e.g. 0xc0000140b8 (an address)
fmt.Println(*p)     // 42, the value at that address

&x means "address of x". *p means "value at the address stored in p". That's the whole mechanism.

Declaring and using

var p *int          // nil, doesn't point anywhere yet
x := 10
p = &x
*p = 20             // write through the pointer
fmt.Println(x)      // 20: x was modified

Pass-by-value, and why you'd want a pointer

Go always passes arguments by value. A function gets its own copy. So this doesn't do what it looks like:

func setToZero(x int) { x = 0 }   // modifies the copy

n := 5
setToZero(n)
fmt.Println(n)     // 5, unchanged

Pass a pointer to mutate the original:

func setToZero(x *int) { *x = 0 }

n := 5
setToZero(&n)
fmt.Println(n)     // 0

Slices, maps, and channels look like they're passed by reference, but really Go passes a copy of their header. Since the header points to shared storage, modifying elements through the copy still affects the original, that's why modifying s[0] in a callee shows up in the caller.

Nil and safety

var p *int
fmt.Println(p == nil)    // true
fmt.Println(*p)          // PANIC: runtime error: invalid memory address

Dereferencing a nil pointer panics. Always guard:

if p != nil { fmt.Println(*p) }

new(T) vs &T{}

// new(T) allocates a zero-valued T and returns a *T
p := new(int)       // *int pointing at 0
*p = 42

// &T{...} is more common, literal syntax, often with fields set
type Point struct { X, Y int }
q := &Point{X: 1, Y: 2}

Idiomatically: use &T{...} for structs (or make for slice/map/channel), new only when you truly want a zero-valued T and no extra initialization.

Auto-dereference on struct fields and methods

You don't need (*p).X. Go auto-dereferences:

type Point struct { X, Y int }
p := &Point{1, 2}
fmt.Println(p.X)       // 1, not (*p).X
p.X = 10                // writes through the pointer

When to use a pointer

  • You need the callee to modify the caller's variable.
  • The struct is large and you want to avoid copying it on every call.
  • You need a sentinel absence value (nil) to distinguish "no value" from "zero value".
  • You're implementing a method that needs to modify the receiver (Chapter 20).

Reasons not to use a pointer:

  • Small values (int, bool, small structs), copying is cheaper than indirection.
  • Immutable data: pointers confuse the reader.
  • Slices, maps, channels: they're already reference-like.

No pointer arithmetic

You can't do p + 1 in Go. This is a deliberate restriction that prevents a huge class of memory-safety bugs. If you need raw byte manipulation, there's the (unsafe) unsafe.Pointer , but you'll never need it in normal application code.

Check your understanding

Practice exercises

EXERCISE 1

Swap two values

Write swap(a, b *int) that swaps the values a and b point to. Verify it works.

Show solution
func swap(a, b *int) { *a, *b = *b, *a }

func main() {
    x, y := 1, 2
    swap(&x, &y)
    fmt.Println(x, y)   // 2 1
}

Go also supports the swap idiom without pointers for local variables: x, y = y, x. But across a function boundary, you need pointers.

EXERCISE 2

Optional field

Design a struct where MiddleName is optional, use *string. Write a FullName(u User) string function that includes the middle name only if it's non-nil.

Show solution
type User struct {
    First, Last string
    Middle      *string   // nil means "not provided"
}

func FullName(u User) string {
    if u.Middle != nil {
        return fmt.Sprintf("%s %s %s", u.First, *u.Middle, u.Last)
    }
    return fmt.Sprintf("%s %s", u.First, u.Last)
}

Using *string vs string gives you a way to distinguish "field not set" from "field set to empty string", important for JSON (Chapter 29) and databases (Chapter 30).

Further reading

Indirection under control.