Back to blog

Saturday, August 15, 2026

Why Did My Struct Change Inside a Function But Not Outside?

Why Did My Struct Change Inside a Function But Not Outside?

Why Did My Struct Change Inside a Function But Not Outside?

Go passes arguments by value. A function that takes a struct (or a method with a value receiver) works on a copy. Field writes inside the function are real — they just die with that copy when the function returns. Pass a pointer, or return the updated struct and assign it, if the caller must see the change.

The failure

This compiles, runs, and looks like it should debit the account. It does not.

package main

import "fmt"

type Account struct {
    Balance int
}

func Debit(a Account, amount int) {
    a.Balance -= amount
    fmt.Println("inside Debit:", a.Balance) // 70
}

func main() {
    acct := Account{Balance: 100}
    Debit(acct, 30)
    fmt.Println("outside:", acct.Balance) // still 100
}

The print inside Debit is not a lie. a.Balance really is 70 — on a different Account that only exists for the duration of the call.

The same trap shows up as a value receiver:

func (a Account) Debit(amount int) {
    a.Balance -= amount
}

That method compiles. It just never updates the value you called it on.

Why this happens

Every assignment, argument, and range iteration copies the struct's header fields. Integers, strings, bools, and arrays are duplicated. The function's a is a new Account sitting on the callee's stack (or wherever the compiler puts it). Nothing in Debit holds a path back to acct in main.

Pointers do not change that rule. Passing *Account still copies the pointer — a machine word. The copy still names the same Account, so writes through it are visible to the caller.

Two extra copies that bite people in the same way:

for _, v := range slice copies each element. Taking &v or calling a pointer method on v mutates the loop variable, not slice[i]. Use the index: for i := range slice { slice[i].Debit(30) }.

Reference-like fields ride along as descriptors. If the struct holds a slice, map, or pointer, the copy shares that backing data. Changing a.Tags[0] can be visible outside even when a.Balance is not. That is not "Go sometimes passes by reference." You copied a pointer that still pointed at the same array.

The fix

Give the function a pointer to the caller's struct, or return a new value and assign it.

func Debit(a *Account, amount int) {
    a.Balance -= amount
}

func (a *Account) Debit(amount int) {
    a.Balance -= amount
}

func main() {
    acct := Account{Balance: 100}
    Debit(&acct, 30)
    acct.Debit(10)
    fmt.Println(acct.Balance) // 60
}

Returning a value is equally valid when the type is a small, copyable value object:

func Debit(a Account, amount int) Account {
    a.Balance -= amount
    return a
}

func main() {
    acct := Account{Balance: 100}
    acct = Debit(acct, 30)
    fmt.Println(acct.Balance) // 70
}

If a type has any mutating method, use pointer receivers on all of its methods. Mixing value and pointer receivers on the same type is how this bug hides in half the API.