← Fundamentals
Guided lesson 6 questions · 3 languages

Changing a caller’s value through a pointer

Write through the pointer: not to a copy, and not to the pointer itself.

A function changes its caller’s variable only by writing through a pointer to it. Changing a copy of the value, or making the pointer lead somewhere else, leaves the caller’s variable as it was.

Before you begin

Why this matters

Go copies every argument, so a function changes its caller’s variable only through a pointer. Knowing where each write lands lets you predict the caller’s values after a call, and choose between a pointer parameter and returning a new value.

What this helps you reason about

  • Write a function that changes two of its caller’s variables, such as a swap.
  • Spot a change made to a copy of the value, or to the pointer itself, that never reaches the caller.
  • Choose between a pointer parameter and returning a value, as most Go code does.

Two fruit beds hold 12 strawberry plants and 30 raspberry canes. A function that swaps the two counts must change two variables that belong to its caller. In Go, the caller passes pointers to them with &, and the function writes through the pointers with *.

Already know this? The short version
  • A function changes a caller’s variable only through a pointer: the caller passes &x, the parameter has type *int, and the function assigns *p = value. For a struct pointer, p.Field = value is short for (*p).Field = value.
  • count := *p copies the value out, and changing count changes nothing the caller sees. p = &local changes only where the parameter points.
  • Go returns several results, so it rarely needs output parameters: return the value with an ok bool or an error, and let the caller assign it.
  • Take *T when the function’s job is to change the caller’s value, or when T is large; otherwise take T and return a new T.

Go to the final check· Go to the practice tickets

Words in this lesson (4)
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.
pass by value
A passing rule in which the parameter is a new variable that starts with a copy of the argument’s value. Assigning to the parameter never changes the caller’s variable.
multiple results
A function that returns more than one value, declared with a list of result types such as (int, int). A call gives all of its results at once, and the caller must receive every one of them.
zero value
The value a variable holds when it is declared without one: 0 for numbers, false for bool, "" for string, and nil for pointers, slices, and maps.

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 / Through a pointer

How can a function change a variable that belongs to its caller?

Arguments, values, and references showed that Go copies every argument, and that a copied pointer still leads to the caller’s variable. A swap puts that to work: it must change two variables that belong to main.

main passes &strawberries and &raspberries, pointers to its two variables. Inside swap, *a is the int that a points to, which is main’s strawberries. So each assignment to *a or *b changes one of main’s variables when it runs. keep holds the first count, so that it is not lost when *a gets the second one.

Go can also swap in one line, *a, *b = *b, *a: the values on the right are read before either assignment happens.

main.go
func swap(a *int, b *int) {
    keep := *a
    *a = *b
    *b = keep
}

strawberries := 12
raspberries := 30
swap(&strawberries, &raspberries)
fmt.Println(strawberries, raspberries) // 30 12

To change a caller’s variable, the caller passes &x and the function assigns to *p.

Coming from Python: A Python function cannot rebind its caller’s names. A Go function can change a caller’s int, but only when the caller passes a pointer to it with &.

Coming from Java: Java has no pointers to local variables, so a Java method cannot swap two of its caller’s ints. In Go, &x gives one.

Experiment

Swap two counts through pointers

main passes the addresses of its two variables. *a and *b are main’s strawberries and raspberries, so each assignment to them changes main’s variables at once. keep holds the first count while it is overwritten.

Why it matters A function can change any variable it has a pointer to, at the moment it writes. When both pointers lead to one variable, every write goes to that variable.

Predict what main’s two variables hold after each line of swap runs. Then change swap(&strawberries, &raspberries) to swap(&strawberries, &strawberries) and predict again.

package main

import "fmt"

func swap(a *int, b *int) {
    keep := *a
    *a = *b
    *b = keep
}

func main() {
    strawberries := 12
    raspberries := 30
    swap(&strawberries, &raspberries)
    fmt.Println(strawberries, raspberries)
}
What to look for

Step into swap and select each frame in turn. a and b each show an address, then → and the int there: 12 and 30. The first stop in swap is on the func line, before keep exists. After *a = *b, main’s strawberries shows 30, so main holds 30 twice for one stop. After *b = keep, main’s raspberries shows 12, and the run prints 30 12. After the edit, a and b show the same address, every line writes 12 into strawberries, and the run prints 12 30.

Expected output

30 12

02 / A copy

Why does a change sometimes stay inside the function?

The change stays inside the function whenever the function changes a copy. A parameter of type int instead of *int gives the function a copy of the number.

A pointer parameter can be copied from too. count := *bed declares a new int that starts with the value of main’s bed. count = count + extra changes count, and nothing writes the sum back. The fix is one assignment through the pointer: *bed = count + extra, or *bed += extra.

The compiler accepts this function. count is read on the right of its own assignment, so the “declared and not used” rule does not catch it.

main.go
func addPlants(bed *int, extra int) {
    count := *bed
    count = count + extra // changes count only
}

bed := 12
addPlants(&bed, 6)
fmt.Println(bed) // 12

Reading *p into a variable gives a copy; the caller sees a change only after an assignment to *p.

Precisely: copies in other places

The same copy hides elsewhere. c := *p for a struct pointer copies every field, and the variable of a for … range loop holds a copy of each element. Taking the address of such a copy, &c, gives a pointer to the copy, not to the original.

Experiment

Change a copy of the caller’s count

count is a new variable that starts with a copy of main’s number. Later changes to count never travel back through bed. Only an assignment to *bed writes main’s variable.

Why it matters When a function copies a value out of a pointer, look for the line that writes it back. Without one, the caller sees no change.

Predict what is printed. Then change count = count + extra to *bed = count + extra and predict again.

package main

import "fmt"

func addPlants(bed *int, extra int) {
    count := *bed
    count = count + extra
}

func main() {
    bed := 12
    addPlants(&bed, 6)
    fmt.Println(bed)
}
What to look for

Step into addPlants; its first stop is on the func line. bed shows an address, then → 12: main’s bed. After count := *bed, count shows 12. After the next line, count shows 18, while bed still shows → 12 and main’s bed shows 12. The run prints 12. After the edit, bed shows → 18 at the closing brace, main’s bed shows 18, and the run prints 18. count is a variable of its own, so it keeps 12 after the edit.

Expected output

12

03 / The pointer itself

What happens when the function changes the pointer itself?

A pointer parameter is itself a variable of the function, holding a copy of the caller’s pointer. Assigning to it, as in basket = &full, makes the parameter point to another int. main’s basket is a different variable, and it still holds 4.

The mistake comes from a habit: “make basket be the new total”. In Go, that means storing the total in the int that basket points to: *basket = full. Watch for an assignment whose left side is a pointer parameter without a *.

Go catches one form of this mistake. basket = 0 does not compile: the compiler reports cannot use 0 (untyped int constant) as *int value in assignment. basket = nil compiles, and makes the parameter point to nothing.

main.go
func harvest(basket *int, picked int) {
    full := *basket + picked
    basket = &full // the parameter now points to full
}

basket := 4
harvest(&basket, 3)
fmt.Println(basket) // 4

Assigning to p changes where the parameter points; assigning to *p changes the caller’s variable.

Coming from C: In C, basket = 0 compiles and makes the parameter a null pointer; Go rejects it, because 0 is not a pointer.

Precisely: changing the caller’s pointer

When the caller’s variable is itself a pointer, and the function must change where it points, the function needs a pointer to that pointer: a parameter of type **int, assigned as *where = &other. Most Go code returns the new pointer instead.

Experiment

Point the parameter at a local

The parameter basket is harvest’s own variable, holding a copy of main’s address. basket = &full stores a new address in that variable. Nothing is written at main’s address.

Why it matters An assignment to a pointer parameter without a * never reaches the caller. To give the caller a value, assign to *p.

Predict what is printed. Then change basket = &full to *basket = full and predict again.

package main

import "fmt"

func harvest(basket *int, picked int) {
    full := *basket + picked
    basket = &full
}

func main() {
    basket := 4
    harvest(&basket, 3)
    fmt.Println(basket)
}
What to look for

Step into harvest. Its first stop is on the func line. basket shows an address, then → 4: main’s basket. After full is computed, it shows 7. After basket = &full, basket shows a different address, then → 7: it now leads to full. main’s basket still shows 4, and the run prints 4. After the edit, basket keeps its first address and shows → 7, main’s basket shows 7, and the run prints 7.

Expected output

4

04 / Result and status

How does a function give back a result and say whether it worked?

A Go function can return several results, so it rarely needs a pointer parameter to give back a result. rowsFor returns the number of rows and a bool, ok, that says whether the number means anything. When perRow is 0 or less, it returns 0 and false: the zero value, which the caller must not use as a row count.

The caller assigns both results, as in rows, ok = rowsFor(36, 0). A returned result replaces the caller’s value, so after a failed call rows holds 0, not an old answer. A function that can fail in several ways returns an error as its last result instead of a bool.

main.go
func rowsFor(cloves int, perRow int) (int, bool) {
    if perRow <= 0 {
        return 0, false
    }
    return cloves / perRow, true
}

rows, ok := rowsFor(40, 8)
fmt.Println(rows, ok) // 5 true

Return the result with an ok bool or an error; Go seldom needs an output parameter.

Coming from C: Where C writes code = f(&n), Go writes n, err := f().

Precisely: output parameters in Go

Some standard functions do write a result through a pointer, such as json.Unmarshal(data, &v) and fmt.Sscan(text, &n). They return an error, and v or n can be trusted only when the error is nil.

Experiment

Return a result and ok together

The caller assigns the returned results to its own variables, so after rows, ok = …, rows holds what this call returned. With _, nothing is assigned to rows, and an old answer stays next to a new false.

Why it matters Use a result only when ok is true. A function that returns its result cannot leave an old value behind, unless the caller throws the result away.

Predict both printed lines. Then change rows, ok = rowsFor(36, 0) to _, ok = rowsFor(36, 0) and predict again.

package main

import "fmt"

func rowsFor(cloves int, perRow int) (int, bool) {
    if perRow <= 0 {
        return 0, false
    }
    return cloves / perRow, true
}

func main() {
    rows, ok := rowsFor(40, 8)
    fmt.Println(rows, ok)
    rows, ok = rowsFor(36, 0)
    fmt.Println(rows, ok)
}
What to look for

Step into each call. rowsFor’s frame lists cloves and perRow only: its results are not listed. After the first call, main shows rows 5 and ok true. The second call goes from the if to return 0, false, and main then shows rows 0 and ok false. The run prints 5 true, then 0 false. After the edit, _ throws the first result away, rows keeps 5, and the run prints 5 true, then 5 false.

Expected output

5 true
0 false

05 / Return instead

When should a function return a value instead?

A pointer parameter is the right tool when the function’s job is to change something the caller already has: swap two variables, or add to a running total that several calls share. Otherwise, take the value and return a new one. A call such as more := grown(bed, 6) leaves the caller’s bed alone, can be used inside an expression, and needs no & at the call.

For a struct, size matters too. Passing a pointer to a large struct avoids copying every field, so functions that work on one often take a pointer even when they only read it. A later lesson, Methods, receivers, and this, makes the same choice for methods.

main.go
// Changes the caller's total: a pointer fits.
func addRain(total *int, mm int) {
    *total += mm
}

// Computes something new: return it.
func grown(plants int, extra int) int {
    return plants + extra
}

Take a pointer to change what the caller has; return a value to give the caller something new.

06 / Try it

Can you explain a fresh example?

Decide for each line of tidy whether it writes through a pointer, to a copy, or to the pointer itself. The next lesson, Stack and heap, shows where the variables that these pointers lead to are kept.

main.go
package main

import "fmt"

func tidy(weeds *int, pulled int, bags *int) {
    *weeds = *weeds - pulled
    pulled = 0
    more := *bags + 1
    bags = &more
}

func main() {
    weeds := 20
    pulled := 8
    bags := 2
    tidy(&weeds, pulled, &bags)
    fmt.Println(weeds, pulled, bags)
}

What is printed?

Reveal the reasoning

12 8 2. *weeds = *weeds - pulled writes through the pointer, so main’s weeds becomes 12. pulled = 0 changes tidy’s copy of 8. more is a new int, 3, and bags = &more makes the parameter point to it, so main’s bags keeps 2.

Practice tickets

  1. POIP-101 · The pond level stays the same after every top-up → Free
  2. POIP-102 · The taller herb never reaches the back of the planter → Sign in · Pro
  3. POIP-103 · The meadow planner loses the last plan and stops on a rows-only request → Sign in · Pro

Keep exploring

Stack and heap →

Primary references