← Fundamentals
Guided lesson 5 questions · 3 languages

Pointers and dereferencing

Taking an address, following it, and what happens when there is nothing to follow.

& takes the address of a variable, and * follows an address to the value stored there. Reading or writing through a pointer reaches that value, not a copy. A pointer that leads nowhere must be checked before it is followed: C leaves the result undefined, Go stops with a panic, and safe Rust has no such reference.

Before you begin

Why this matters

Most pointer mistakes are about which variable a * reaches, or about following a nil pointer. Go stops the second one with a panic, which is easier to find than a wrong number, but it still stops the program.

What this helps you reason about

  • Predict which variable changes when code writes through a pointer.
  • Explain why warm.Level works when warm is a pointer, and why *warm.Level does not compile.
  • Spot a pointer that may be nil, and check it before it is followed.

A home has a radiator in each room, and a dial sets each one’s level from 0 to 5. In Go, a pointer to the hall radiator lets other code read and turn its dial. This lesson follows the two operators that make this work, & and *.

Already know this? The short version
  • &x gives a pointer to x, of type *T; *p follows it, and *p = v stores v in that variable. A copy made with := does not follow later changes.
  • For a pointer to a struct, p.Field means (*p).Field: Go adds the * for you. *p.Field means *(p.Field), and does not compile for an int field.
  • Following a nil pointer, including p.Field, panics with invalid memory address or nil pointer dereference; check p != nil first.
  • **T is a pointer to a pointer: each * follows one address. Go has no pointer arithmetic.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
pointer
A value that holds the address of a variable, written *int for a pointer to an int. &count makes a pointer to count, and *p reads or changes the variable that p points to.
dereference
To follow a pointer to the variable it points to, written *p. Go does it for you when you use a field through a pointer: p.Level means (*p).Level.
field
One named part of a struct, such as Kg in a Tag. tag.Kg reads or changes that field of the struct tag.
nil
The zero value of pointers, slices, maps, channels, functions, and interfaces such as error. It means the value refers to nothing yet.
panic
Go’s way of stopping a program because of a mistake in the code. panic("message") stops the current function and every function that called it, running their deferred calls on the way. Unless a deferred call recovers, the program prints panic: message and a stack trace, then exits.
pointer to a pointer
A pointer whose value is the address of another pointer, written **int for a pointer to a pointer to an int. *pp is the inner pointer, and **pp is the int it points to.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / & and *

What do & and * do?

References, pointers, and handles showed that a pointer holds where a value is, not the value. This lesson is about the two operators that make and follow one.

& gives a pointer to a variable. valve := &hall makes valve a pointer to hall, of type *int. * in front of a pointer goes the other way: it follows the pointer to the variable. Following a pointer is called dereferencing. *valve reads hall, and *valve = 4 stores 4 in hall. In a type, *int means “pointer to int”; in an expression, *valve means “follow valve”.

A copy does not follow. before := hall copies the number 2 at that moment, so a later write through valve does not reach before. Without the *, valve = 4 does not compile: cannot use 4 (untyped int constant) as *int value in assignment.

main.go
hall := 2
before := hall // copies the number 2
valve := &hall // valve points to hall
*valve = 4     // stores 4 in hall
fmt.Println(hall, before, *valve) // 4 2 4

&x gives a pointer; *p follows it, and a write through *p changes the variable it points to, not a copy.

Coming from Java: A Go int or struct variable holds the value itself; only an explicit pointer, made with &, reaches it from elsewhere.

Precisely: what the specification says

The Go specification calls *p a pointer indirection, and says that if p is nil, evaluating *p causes a run-time panic. & can also be applied to a composite literal: &Radiator{Room: "hall"} makes a new variable and gives its address.

Experiment

Write through a pointer, next to a copy

&hall gives where hall is, so *valve = 4 stores 4 in hall. before got a copy of the number 2 when its line ran; nothing connects it to hall afterwards. *valve reads whatever valve points to now.

Why it matters A write through a pointer reaches exactly the variable whose address it holds; a copy never sees it.

Predict the three numbers. Then change valve := &hall to valve := &before and predict again.

package main

import "fmt"

func main() {
    hall := 2
    before := hall
    valve := &hall
    *valve = 4
    fmt.Println(hall, before, *valve)
}
What to look for

Each variable appears in the panel after its line runs. valve shows an address, then → and the int there: 2. After *valve = 4, hall shows 4 and valve shows → 4, while before still shows 2. The run prints 4 2 4. After the edit, valve holds before’s address, the write changes before to 4, hall stays 2, and the run prints 2 4 4.

Expected output

4 2 4

02 / Fields

How do you reach a field through a pointer?

A struct is often reached through a pointer. warm := &hall makes warm a pointer to the whole Radiator. (*warm).Level follows warm and then uses the field Level. Go writes this step for you: warm.Level means exactly (*warm).Level, so code through a pointer looks the same as code on the struct itself.

The parentheses matter when you write the * yourself. The . is applied before the *, so *warm.Level means *(warm.Level): take the int field, then follow it. An int is not a pointer, so the compiler reports invalid operation: cannot indirect warm.Level (variable of type int).

main.go
hall := Radiator{Room: "hall", Level: 1}
warm := &hall
warm.Level = warm.Level + 1       // 2
(*warm).Level = (*warm).Level + 1 // 3: the same thing
fmt.Println(hall.Level)           // 3

Through a pointer to a struct, p.Field is (*p).Field; Go adds the * for you.

Coming from C: Go has no ->: warm.Level works for a struct and for a pointer to one.

Precisely: methods

Go does the same for method calls: it adds a * or an & so that the method gets the receiver it was declared with. Methods, receivers, and this covers this later.

Experiment

Reach a field through a pointer two ways

warm.Level means (*warm).Level: Go follows the pointer for you. In *warm.Level, the . is applied first, so * is applied to an int, which is not a pointer.

Why it matters Through a pointer to a struct, write p.Field; if you write the * yourself, put it in parentheses: (*p).Field.

Predict the level printed. Then change (*warm).Level = (*warm).Level + 1 to *warm.Level = *warm.Level + 1 and read the compiler error.

package main

import "fmt"

type Radiator struct {
    Room  string
    Level int
}

func main() {
    hall := Radiator{Room: "hall", Level: 1}
    warm := &hall
    warm.Level = warm.Level + 1
    (*warm).Level = (*warm).Level + 1
    fmt.Println(warm.Room, hall.Level)
}
What to look for

warm shows an address, then → main.Radiator {Room: "hall", Level: 1}. Each of the two lines adds 1 to Level, and hall and warm’s target change together: 2, then 3. The run prints hall 3. After the edit, the program does not compile, and the compiler reports invalid operation: cannot indirect warm.Level (variable of type int).

Expected output

hall 3

03 / Nowhere

What happens when a pointer leads nowhere?

A pointer that points to no variable is nil, the zero value of every pointer type, as Absence, zero, and empty values showed. var attic *Radiator declares a nil pointer, and the stepper shows it as *main.Radiator nil. Here a nil pointer is meant: the attic has no radiator yet.

Following a nil pointer stops the program with a panic: runtime error: invalid memory address or nil pointer dereference. That includes attic.Level, because Go turns it into (*attic).Level, so a field read is a dereference too. Check the pointer itself first, with r == nil; comparing a pointer never follows it.

main.go
func levelOf(r *Radiator) int {
    if r == nil {
        return -1 // no radiator
    }
    return r.Level
}

Check a pointer that may be nil before you use a field through it; following nil panics.

Coming from Java: A nil dereference is Go’s NullPointerException: it happens at run time, and the compiler does not warn.

Experiment

Follow a nil pointer

r.Level means (*r).Level, so it follows r. When r is nil there is no variable to follow, and the Go runtime stops the program with a panic. The check r == nil compares the pointer itself, which never panics.

Why it matters Check a pointer that may be nil before you use a field through it; the compiler does not check it for you.

Predict the two numbers. Then delete the three lines of the if in levelOf and predict again.

package main

import "fmt"

type Radiator struct {
    Room  string
    Level int
}

func levelOf(r *Radiator) int {
    if r == nil {
        return -1
    }
    return r.Level
}

func main() {
    hall := Radiator{Room: "hall", Level: 2}
    var attic *Radiator // no radiator in the attic yet
    fmt.Println(levelOf(&hall), levelOf(attic))
}
What to look for

attic shows *main.Radiator nil: a pointer of type *Radiator with no target. In the first call, r shows an address and → the hall Radiator. In the second, r shows nil, and stepping stops on return -1. The run prints 2 -1. After the edit, the second call stops on return r.Level with the message runtime error: invalid memory address or nil pointer dereference, and nothing is printed.

Expected output

2 -1

04 / Two steps

Can a pointer lead to another pointer?

A pointer is a variable too, so it has an address. panel := &valve makes panel a pointer to a pointer, of type **int. *panel is valve itself, and **panel follows both steps to hall. The stepper shows both steps: panel = 0x… → 0x… → 2.

So *panel = &attic changes where valve points, and **panel = 5 changes what valve points to. Go code needs this rarely: when code must change which variable another pointer leads to.

main.go
hall := 2
valve := &hall
panel := &valve   // a pointer to valve, of type **int
**panel = 5       // follows panel, then valve: hall is 5
fmt.Println(hall) // 5

Each * follows one pointer: *pp is the inner pointer, and **pp is the variable it points to.

05 / Try it

Can you explain a fresh example?

Follow each * and decide which variable each line changes. The next lesson in this section is about changing a caller’s value through a pointer.

main.go
package main

import "fmt"

func main() {
    hall := 2
    attic := 0
    valve := &hall
    panel := &valve
    **panel = 5
    *panel = &attic
    *valve = 1
    fmt.Println(hall, attic, **panel)
}

What is printed?

Reveal the reasoning

5 1 1. **panel = 5 follows panel to valve, then valve to hall, so hall becomes 5. *panel = &attic changes valve itself: valve now points to attic. So *valve = 1 changes attic, and **panel follows panel, then valve, to attic, which holds 1.

Practice tickets

  1. POID-101 · Watering a plant in the app does not reset its dry days → Free
  2. POID-102 · The wet-coat count is wrong when a hook is empty → Sign in · Pro
  3. POID-103 · Devices on an extension lead show the lead, not the wall socket → Sign in · Pro

Keep exploring

Changing a caller’s value through a pointer →

Primary references