Pointers

intermediate go pointers

A pointer is a value that holds the memory address of another value. Instead of “the data,” a pointer is “where the data lives.”

Two operators do all the work:

  • &x gives us the address of x (a pointer to it).
  • *p gives us the value that the pointer p points at (we call this dereferencing).
x := 10
p := &x          // p is a *int — a pointer to x
fmt.Println(*p)  // 10 — follow the pointer to read the value
*p = 20          // write through the pointer
fmt.Println(x)   // 20 — x changed because p pointed at it

Think of & as “give me the address of” and * as “go to that address.”

No pointer arithmetic

If we’ve used C, forget one habit. Go has no pointer arithmetic. We cannot do p++ to walk through memory. We can only point at something, read it, or write it.

Why? Safety. Pointer arithmetic is a huge source of bugs and security holes. Go’s garbage collector also needs to know exactly what’s a pointer, so it bans the math. This makes Go pointers much safer than C pointers.

Why pass pointers

Two real reasons we reach for pointers:

1. Mutation. Go passes everything by value (a copy). If a function should change the caller’s data, it needs a pointer.

func doubleCopy(n int)  { n *= 2 }      // changes a copy — useless
func doublePtr(n *int)  { *n *= 2 }     // changes the real value

v := 5
doubleCopy(v)
fmt.Println(v)  // 5 — unchanged
doublePtr(&v)
fmt.Println(v)  // 10 — changed

2. Avoid copying big structs. Passing a large struct by value copies every field. Passing a pointer copies just one address. Cheaper.

type BigConfig struct { /* ...dozens of fields... */ }
func process(c *BigConfig) { /* no expensive copy */ }

new vs &T{}

Two ways to allocate and get a pointer. They mostly do the same thing.

  • new(T) allocates a zeroed T and returns a *T.
  • &T{...} makes a struct literal (optionally with field values) and returns its address.
type User struct{ Name string }

a := new(User)          // *User, Name is "" (zero value)
b := &User{Name: "M"}   // *User, Name is "M"
fmt.Println(a.Name, b.Name) // "" M

In practice we almost always use &T{...} because it lets us set fields right away. new is rare — handy mainly when we just want a zeroed pointer to a basic type. Note Go auto-dereferences for us: we write a.Name, not (*a).Name.

nil pointers

A pointer that points at nothing is nil. Reading a field through a nil pointer panics, so we guard against it.

var u *User           // nil pointer
// fmt.Println(u.Name) // PANIC: nil pointer dereference
if u != nil {
    fmt.Println(u.Name)
}

The classic “nil pointer dereference” panic is just us following a pointer that points nowhere. Always check before dereferencing if a pointer could be nil.

Interview soundbite

& takes an address, * follows it, and Go deliberately has no pointer arithmetic for safety and the garbage collector. We pass pointers for two reasons: to mutate the caller’s data (since Go copies by value) and to avoid copying large structs. We usually allocate with &T{...} over new, and we guard against nil pointers to avoid the dereference panic.