Appendix E. Passing by value or by reference
This appendix clarifies how parameter passing works in Go and how it impacts program design. The central message is that Go always passes arguments by value, meaning every function call copies its parameters. The practical consequence is that mutating state across function boundaries requires passing pointers to share the same underlying data. It also highlights shallow copy pitfalls with pointer-typed fields, explains that methods follow the same rules as functions, and examines special container types—slices, maps, and channels—whose small headers hide internal pointers. The chapter concludes with pragmatic guidelines for choosing between values and pointers.
Mechanically, passing a struct by value creates a distinct copy, so changes made inside the callee do not affect the caller’s original. Passing a pointer copies only the pointer (type + address), preserving access to the same memory and enabling mutation. Because copies are shallow, a value parameter that contains pointer fields still points to the original data, making unintended updates possible if the pointees are modified. Methods are simply syntax over functions with an extra receiver parameter; the same value-versus-pointer trade-offs apply to method receivers.
Slices, maps, and channels are lightweight descriptors that reference underlying storage. As a result, functions can reorder slice elements or modify map/channel contents and those changes are visible to the caller, even though the descriptors themselves are passed by value. However, operations that change a slice’s length/capacity (such as append) must return the new slice. The recommendations are: prefer passing by value by default; use pointers when you need to mutate or for rare, measured performance reasons; avoid passing pointers to slices/maps/channels (return updated values instead), with narrow exceptions like unmarshalling into a slice; and consider writing explicit deep-copy functions when you must ensure no memory is shared.
FAQ
Does Go pass function parameters by value or by reference?
Go always passes parameters by value. That means every argument is copied when a function or method is called. If you pass a pointer, the pointer itself is copied, but it still points to the same underlying data.Why didn’t my function update the caller’s struct when I used a non-pointer parameter?
If your function signature is likefunc AddPoints(p Player, points int), the parameter p is a copy. Mutating it changes only the copy, not the original in the caller’s scope. Use a pointer parameter (for example, *Player) to modify the caller’s data.When should I use a pointer parameter?
Use a pointer when the function needs to mutate the input (e.g., increment a field, replace a value, lock a mutex). Example:func AddScore(p *Player, delta int) { p.Score += delta }. Passing a pointer shares the same underlying data with the caller.Is using a pointer parameter the same as “pass by reference”?
No. Go still passes by value. When you pass a pointer, the value being copied is the pointer (address + type), not the pointee. Because both copies of the pointer reference the same memory, mutations via the pointer are observable by the caller.What is a shallow copy, and why does it matter?
A shallow copy duplicates the immediate fields of a value but not the data those fields point to. For a struct containing pointer fields, copying the struct creates a new struct that still points to the original inner data. Mutating that inner data via the copy affects the original. Avoid mutating pointees in functions that take values by copy.How do methods relate to functions with respect to value vs pointer?
Methods are like functions where the first parameter is the receiver. A value receiver(p Player) copies the receiver; a pointer receiver (p *Player) lets you mutate the original. Choose pointer receivers when methods need to modify the receiver’s state.How do slices behave when passed to functions?
A slice is a small header (pointer, len, cap). Passing it copies the header, not the underlying array. - Operations that change the header (likeappend when it grows) must return the new slice: s = append(s, x). - Operations that rearrange elements (e.g., slices.Sort) can mutate the underlying array in place, so changes are visible to the caller without returning a new slice.Should I pass a slice or map by pointer?
Usually no. Slices and maps are lightweight headers that reference underlying storage. Pass them by value in most cases. Return the updated slice when you need to grow or shrink it (likeappend does). An exception is APIs that need to set the slice/map variable itself (e.g., json.Unmarshal requires &slice so it can update len/cap/address).How do maps and channels behave as parameters?
Like slices, maps and channels are references to internal structures. Passing them by value copies the header, but both copies refer to the same underlying data. Functions can add/remove map entries or send/receive on channels, and the effects are visible to the caller.Is passing by pointer a performance optimization?
Sometimes, but it’s a minor one. For very large structs, avoiding copies can help, but measure before optimizing. Prefer passing by value as the default for clarity and safety, and use pointers when mutation is required or profiling shows a clear benefit. If you need independent data, consider writing a deepCopy to avoid shared memory.
Learn Go with Pocket-Sized Projects ebook for free