15 min read · Beginner-friendly · Real code you can paste into Go Playground
The bug that confuses every Go beginner
You pass your slice to a function. The function changes some values. You print the slice back in main... and some changes are there, some aren't.
Sound familiar? This article explains exactly why — with diagrams, not walls of text.
Our data — a fleet of vehicles
Two structs, one slice. Everything in this article uses these types.
type VehicleType struct {
TwoWheeler, ThreeWheeler, FourWheeler bool
}
type Vehicle struct {
Model, Color string
Type VehicleType // nested by value — not a pointer
}
type Fleet []Vehicle
Fill it with two vehicles:
fleet := Fleet{
{Model: "Bike", Color: "Red", Type: VehicleType{TwoWheeler: true}},
{Model: "Car", Color: "Blue", Type: VehicleType{FourWheeler: true}},
}
What is a slice? (the sticky note model)
Before anything else, burn this mental model in — a slice variable is just a 3-field sticky note. The real data lives on the heap.
When you pass fleet to a function, Go copies the sticky note — not the parking lot.
Pass by value — a photocopy of the sticky note
The function gets its own header copy. Both copies point at the same backing array.
func byValue(fleet Fleet) {
fleet[0].Color = "Green" // ✅ writes to shared heap — caller SEES it
fleet[0].Type.TwoWheeler = false // ✅ nested struct is on the heap too
fleet = append(fleet, Vehicle{}) // ❌ updates ONLY the function's copy of len
}
What the caller sees after byValue(fleet):
| Action | Visible to caller? |
|---|---|
fleet[0].Color → "Green" |
✅ Yes |
fleet[0].Type.TwoWheeler → false |
✅ Yes |
len(fleet) → 3 from append |
❌ No — still 2 |
💡 Seems quite easy: Repainting a car already in the lot? Everyone sees it. Driving a new car into YOUR copy of the address? The owner's map still shows 2 cars.
Pass by reference — handing over the key
Pass *Fleet so the function can update the caller's header — including length.
func byRef(fleet *Fleet) {
(*fleet)[0].Color = "Yellow" // ✅ visible to caller
*fleet = append(*fleet, Vehicle{}) // ✅ len change visible to caller too
}
// call it:
byRef(&fleet)
What the caller sees after byRef(&fleet):
| Action | Visible to caller? |
|---|---|
fleet[0].Color → "Yellow" |
✅ Yes |
len(fleet) → 3 from append |
✅ Yes |
New vehicle at fleet[2]
|
✅ Yes |
🔑 As simple as that: You handed the function the actual key to the parking office. It can repaint cars AND add new bays — and you'll see all of it.
Deep clone — two completely separate parking lots
Need zero shared state? Use copy() to build a brand-new backing array.
clone := make(Fleet, len(fleet))
copy(clone, fleet) // new backing array at a new address
clone[0].Color = "Purple" // original fleet untouched ✅
🧬 When to use this: Sorting or filtering for display, transforming data for an API response, or any time changes must not affect the source slice.
Quick reference
| Action | Pass by Value | Pass by Reference |
|---|---|---|
fleet[0].Color = ... |
✅ Caller sees it | ✅ Caller sees it |
fleet[0].Type.X = ... |
✅ Caller sees it | ✅ Caller sees it |
append(fleet, vehicle) |
❌ NOT visible | ✅ Caller sees it |
3 rules — commit these to memory
Rule 1 ✅ — Mutate existing element → always visible, value or pointer.
Rule 2 ⚠️ — append → only visible when you pass *Fleet (pointer).
Rule 3 🧬 — Total isolation → use copy() for a brand-new backing array.
Full runnable example
Paste this into Go Playground and run it:
package main
import "fmt"
type VehicleType struct {
TwoWheeler, ThreeWheeler, FourWheeler bool
}
type Vehicle struct {
Model, Color string
Type VehicleType
}
type Fleet []Vehicle
func byValue(f Fleet) {
f[0].Color = "Green" // ✅ visible to caller
f = append(f, Vehicle{}) // ❌ NOT visible to caller
}
func byRef(f *Fleet) {
(*f)[0].Color = "Yellow" // ✅ visible to caller
*f = append(*f, Vehicle{}) // ✅ visible to caller
}
func main() {
fleet := Fleet{
{Model: "Bike", Color: "Red", Type: VehicleType{TwoWheeler: true}},
{Model: "Car", Color: "Blue", Type: VehicleType{FourWheeler: true}},
}
byValue(fleet)
fmt.Println(fleet[0].Color, len(fleet)) // Green 2
byRef(&fleet)
fmt.Println(fleet[0].Color, len(fleet)) // Yellow 3
}
What about the nested VehicleType?
Because VehicleType is embedded by value inside Vehicle (not a pointer), it lives contiguously in memory with the rest of the struct. No special rules — it copies and mutates exactly like any other field.
If it were a pointer (Type *VehicleType), that would be a different story — but that's a topic for another article.
Rahul Tomer
Turning coffee into bugs
My first blog, found this useful? Let me know in comments.





Top comments (0)