skip to content

Go passes every argument by value — how do you write a function that modifies the caller's struct?

level: juniorimportance: must knowfreq 88%

answer

  1. Go has exactly one calling convention
  2. The parameter is a fresh copy
  3. Give the callee an address instead
  4. &x makes it, p.Field writes through it
  5. Rebinding the pointer changes nothing outside

basics

~20 s

Pass a pointer. A function parameter of type Player receives a copy, so writes to it die with the call. Declare the parameter as *Player, call it with &pl, and write p.HP = 40 — Go dereferences the selector for you.

solid answer

~50 s

Go has no pass-by-reference at all: every argument, result and assignment copies the value. So `func heal(p Player) { p.HP += 10 }` increments a copy and the caller's `Player` is untouched. To mutate the caller's variable you pass its address: `func heal(p *Player)` called as `heal(&pl)`. Inside, `p` is a `*Player` holding the address of the caller's variable, and `p.HP += 10` writes through it — the selector auto-dereferences, so `(*p).HP` is legal but nobody writes it. The same rule explains the second half of the answer: a pointer parameter is *also* passed by value, so reassigning `p = &other` inside the function is invisible to the caller; only writes *through* the pointer escape the call. Use a pointer when you must mutate, when the struct is large, or when the type already has pointer methods; otherwise a value keeps the API simpler and the data unshared.

code

go · 22 lines
go
type Player struct {
	Name string
	HP   int
}

func heal(p Player) {
	// writes to a copy; the caller never sees it
	p.HP += 10
}

func healPtr(p *Player) {
	// writes through the caller's address; p.HP means (*p).HP
	p.HP += 10
}

func tick() {
	pl := Player{Name: "ada", HP: 50}
	heal(pl)
	fmt.Println(pl.HP) // 50
	healPtr(&pl)
	fmt.Println(pl.HP) // 60
}

go deeper

for a junior

Be ready to state the rule in one sentence — Go copies every argument — and then show the two-line fix: change the parameter to *Player and call it with &pl. Know that p.Field writes through the pointer.

for a middle

Explain that the pointer is itself copied, so writing through it is visible but rebinding it is not, and be able to say what &x and *p each produce and what a nil dereference does at run time.

for a senior

Show the judgment behind the choice: when a pointer parameter buys mutation or avoids a measurable copy, and when it costs you a possible nil and an aliasing question in review. Be able to say what you would check before switching an API to pointers.

for a principal

Frame it as an API contract others depend on: a value parameter promises no mutation and no retention, a pointer parameter promises neither, and flipping one on an exported function changes behaviour for every caller without breaking the build.

## The one rule Go is a strictly **pass-by-value** language. There is no `ref` parameter, no implicit aliasing, no "objects are references" special case. Every function argument, every return value, every assignment and every channel send copies the value bit for bit. Once you internalise that single sentence, most of the surprises in this area disappear. ```go type Player struct { Name string HP int } func heal(p Player) { p.HP += 10 } ``` Calling `heal(pl)` copies the whole `Player` struct — both fields — into the parameter `p`. The function increments a field of that copy, the copy is discarded when the call returns, and the caller's `pl.HP` is exactly what it was. Nothing is "optimised away"; the compiler may well keep the copy in registers, but the *semantics* are a copy. ## Making the mutation visible: `&x` and `*T` To let the callee reach the caller's variable, hand it the variable's **address**: ```go func heal(p *Player) { p.HP += 10 } pl := Player{Name: "ada", HP: 50} heal(&pl) // pl.HP is now 60 ``` Two operators do the work, and they are mirror images: - `&x` is the **address-of** operator. Applied to a variable of type `T` it yields a value of type `*T` — a pointer that identifies where `x` lives. - `*p` is the **dereference** operator. Applied to a value of type `*T` it yields the `T` stored there. Confusingly, `*` also appears in *types*: `*Player` read in a declaration is "pointer to Player", while `*p` read in an expression is "the Player that p points at". Go removes most of the punctuation you would write in C: for a `p` of type `*Player`, the selector `p.HP` automatically means `(*p).HP`. Both compile; the short form is universal Go. There is no `->` operator in the language. ## The pointer itself is still a value This is the half candidates usually miss. A `*Player` parameter is copied like everything else — what is copied is the *address*, not the struct. Consequences: ```go func reset(p *Player) { *p = Player{} // caller SEES this: writes through the address p = &Player{HP: 1} // caller does NOT see this: rebinds the local copy } ``` Writes *through* the pointer reach the caller's memory. Rebinding the pointer variable only changes the callee's copy of the address. If a function genuinely needs to make the caller's *variable* point somewhere else, it needs a `**Player`, or — far more idiomatic in Go — it should return the new value. ## Dereferencing nothing A pointer's zero value is `nil`. Dereferencing it — `*p`, or `p.HP`, which dereferences implicitly — panics at run time with `invalid memory address or nil pointer dereference`. A function that accepts a `*Player` is implicitly accepting "or nothing", which is one real cost of choosing a pointer parameter: values cannot be absent, pointers can. ## No pointer arithmetic Go deliberately omits `p + 1`, `p++` on pointers, and casts between pointer types. A `*Player` can only be dereferenced, compared for equality, or assigned. That is what allows the garbage collector to know precisely which words are pointers, and it is why bounds-checked slices — not walking pointers — are how Go traverses memory. (The `unsafe` package can break this, at the cost of leaving the safe language.) ## Choosing between a value and a pointer parameter Prefer a **value** when the function only reads, the struct is small, and you want callers to be certain nothing is retained or mutated. Prefer a **pointer** when you must mutate the caller's data, when the struct is large enough that copying it per call is measurable, when the value must not be copied at all (anything containing a `sync.Mutex` must be passed by pointer once it has been used), or when the type's methods already use pointer receivers so mixing would be inconsistent. "Big structs must be pointers" is a benchmarking question, not a rule — a two- or three-word struct is usually cheaper to copy than to chase through memory. Finally, note what this rule does *not* say. It says the *value* is copied, not that the data behind it is. Slices, maps, channels, function values and interface values all contain a pointer inside them, so copying such a value copies a handle to shared data — which is why a function can mutate the caller's map without ever taking its address.

  • If the callee gets a copy anyway, why not just take a pointer everywhere?
    Because a value parameter is a guarantee: the callee cannot mutate your data and cannot retain it. Values also cannot be nil, so there is no dereference panic to defend against, and small structs are often cheaper to copy than to chase through an indirection. Reach for a pointer when you must mutate, when the type must not be copied, or when a benchmark shows the copy matters.
  • Inside a function with a *Player parameter, why does p = &Player{} not affect the caller?
    Because the pointer itself was passed by value. `p` is the callee's own copy of an address; reassigning it points the local copy somewhere new and leaves the caller's variable alone. Only writes through the pointer — `*p = ...` or `p.Field = ...` — reach the caller's memory. To replace the caller's value entirely, write `*p = Player{}` or return the new value.
  • Is p.HP different from (*p).HP when p is a *Player?
    No. Go's selector automatically dereferences a pointer, so `p.HP` compiles to exactly `(*p).HP`. Both forms are legal and identical; idiomatic Go always writes the short one. Go has no `->` operator at all. The only place the explicit `*` is required is when you want the whole struct, as in `snapshot := *p`.

Passing a value is handing someone a photocopy of a form: they can scribble on it all day and your original is untouched. Passing a pointer is handing them the filing-cabinet drawer number instead.

saying these in an interview costs you the question

  • Says Go passes large structs by reference automatically
  • Thinks &x makes a copy of the struct
  • Claims Go has a pass-by-reference mode like C++'s T&
  • Believes reassigning a pointer parameter is visible to the caller
  • Writes (*p).Field believing p.Field means something else
  • Assumes a pointer parameter is always faster than a value