๐Ÿ“PointersLESSON

Pointers

Pointers are one of Go's most important and practical features. They let you work directly with memory addresses โ€” passing references to data rather than copies, and enabling functions to modify variables that live outside their own scope.

Memory Addresses and the & Operator

Every variable in your program lives at a specific location in memory. The address-of operator & returns that location as a pointer value.

The value of p is not 42 โ€” it is the address where 42 is stored. On most 64-bit systems, a pointer is 8 bytes regardless of what it points to.

The * Operator: Dereferencing

To read or write the value that a pointer points to, use the dereference operator *. This follows the pointer to the actual variable.

Modifying *p is exactly the same as modifying n directly. The pointer is just a path that leads back to the original variable.

Pointer Types

A pointer's type encodes both the fact that it is a pointer and what type it points to. The type *int is "pointer to int", *string is "pointer to string", and so on.

The type system prevents you from, for example, assigning a *string where a *int is expected โ€” type safety applies to pointers just as it does to regular values.

Why Use Pointers?

1. Modify a caller's variable

Without pointers, a function receives a copy of its arguments. Changes to that copy never escape back to the caller.

2. Avoid copying large structs

When a struct has many fields, passing it by value copies every single field on every call. Passing a pointer copies just the 8-byte address โ€” far cheaper for large types.

The Zero Value of a Pointer: nil

An uninitialized pointer has the zero value nil, which means it points to nothing. Dereferencing a nil pointer causes a runtime panic โ€” one of the most common mistakes in Go.

Always guard before dereferencing a pointer that might be nil:

This pattern โ€” check, then dereference โ€” is the standard defense against nil pointer panics.

Allocating with new

The built-in new(T) function allocates memory for a zero-value T and returns a *T. It is handy when you want a pointer without first declaring a named variable.

In practice, new is used less often than you might expect. Most of the time, you either take the address of an existing variable with &, or use a struct literal with & to get a pointer to a new struct value.

Value Receivers vs Pointer Receivers (Preview)

When you define methods on a struct, the receiver can be a value or a pointer. This is the same trade-off as regular function parameters:

  • Value receiver (s MyStruct): the method gets a copy. Reading only, or for small structs.
  • Pointer receiver (s *MyStruct): the method can mutate the original. Required when the method must change fields.

Go automatically takes the address of c when you call a pointer receiver method on an addressable value, so c.Increment() compiles and works even though c is not declared as a *Counter. You will explore this fully in the Structs module.

Pointer Indirection in Method Calls

Go quietly bridges the gap between value and pointer receivers at call sites. When you call a pointer-receiver method on an addressable value variable, Go rewrites the call as (&v).Method() for you. The reverse also applies: a value-receiver method called on a pointer is rewritten as (*p).Method().

The addressability constraint is the key limit: Go can only auto-take the address of a variable, not of an expression that produces a temporary value. A composite literal used directly as a call target is not addressable, so Counter{}.Inc() is a compile error โ€” there is no variable whose address Go can use.

The practical takeaway: declare your receiver as a variable (c := Counter{}) and Go handles the & and * mechanics transparently.

Choosing a Value or Pointer Receiver

The Tour of Go gives clear guidance on which receiver type to choose. The decision affects correctness, performance, and interface satisfaction.

Use a pointer receiver when:

  • The method must modify the receiver. A value receiver gets a copy; mutations are discarded when the method returns.
  • The struct is large. Copying a large struct on every call wastes CPU and stack space; a pointer costs only 8 bytes.
  • Consistency. If any method on a type uses a pointer receiver, all methods should โ€” mixing creates confusion about which calls mutate the original and which do not, and it breaks interface satisfaction in subtle ways.

Use a value receiver when:

  • The method only reads the receiver and the type is small enough that copying is cheap.
  • You want call-site immutability โ€” callers pass a value and the method cannot affect their copy.
  • The type is a primitive alias (e.g., type Celsius float64) where pointer overhead is not justified.

The consistency rule in practice

When a type satisfies an interface, Go requires that the method set matches. A pointer receiver method is only in the method set of *T, not T โ€” so keeping all methods on pointer receivers avoids the common mistake of passing a value where a pointer is needed to satisfy an interface.

Knowledge Check

What does `&x` return?

How do you read the value at a pointer `p`?

You have `var c Counter` and `func (c *Counter) Inc()`. How do you call Inc?