Mutability and Data Sharing
Mutability and Data Sharing
A slice and a map are small headers that point at data. Passing them copies the header, not the elements. The boring default is: if a function should not change the caller’s data, copy first — or do not write through the header.
Mental model
A slice header is three words: pointer to an array, length, capacity. func f(s []int) copies those three words. Both headers can point at the same array. s[i] = x is visible to the caller. append is visible to the caller when the array has spare capacity; if append must grow, it may allocate a new array and then only the returned header sees the new element.
A map is already a pointer under the hood. func f(m map[int]string) copies the pointer. m[k] = v and delete(m, k) mutate the same map.
Structs and arrays are copied in full. Slices, maps, and pointers are the usual ways two functions share memory by accident.
Worked examples
Case 1: Writing through a slice header
Save as mark_paid.go. markPaid zeros the first item. order in main changes, because both headers share the array.
// mark_paid.go
package main
import "fmt"
func markPaid(cents []int) {
if len(cents) == 0 {
return
}
cents[0] = 0
}
func main() {
order := []int{450, 120, 90}
markPaid(order)
fmt.Println(order)
}Run:
go run mark_paid.goOutput:
[0 120 90]
That is useful when the function is meant to edit the order. It is a bug when the function was supposed to preview a discount.
Case 2: copy gives you a private array
Save as mark_paid_copy.go. Allocate a new slice, copy the elements, mutate the copy.
// mark_paid_copy.go
package main
import "fmt"
func markPaid(cents []int) {
if len(cents) == 0 {
return
}
cents[0] = 0
}
func main() {
order := []int{450, 120, 90}
safe := make([]int, len(order))
copy(safe, order)
markPaid(safe)
fmt.Println("order:", order)
fmt.Println("safe:", safe)
}Run:
go run mark_paid_copy.goOutput:
order: [450 120 90]
safe: [0 120 90]
append([]int(nil), order...) is another way to copy. copy makes the length obvious. Either is fine. safe := order is not a copy; it copies the header only.
The trap
append on a slice that still has capacity writes into the original array. The caller’s slice length does not grow, but its elements can change.
Save as add_item.go:
// add_item.go
package main
import "fmt"
func addItem(cents []int, extra int) []int {
return append(cents, extra)
}
func main() {
order := make([]int, 2, 4)
order[0], order[1] = 450, 120
next := addItem(order, 90)
next[0] = 1
fmt.Println("order:", order)
fmt.Println("next:", next)
}Run:
go run add_item.goOutput:
order: [1 120]
next: [1 120 90]
order never got a third element, but next[0] = 1 overwrote order[0]. Spare capacity made the headers aliases.
The fix is to copy into a new array before append, so growth cannot land in the caller’s backing store:
// add_item_copy.go
package main
import "fmt"
func addItem(cents []int, extra int) []int {
out := make([]int, len(cents), len(cents)+1)
copy(out, cents)
return append(out, extra)
}
func main() {
order := make([]int, 2, 4)
order[0], order[1] = 450, 120
next := addItem(order, 90)
next[0] = 1
fmt.Println("order:", order)
fmt.Println("next:", next)
}Run:
go run add_item_copy.goOutput:
order: [450 120]
next: [1 120 90]
If addItem is supposed to share — a builder that owns the buffer — return the new header and stop using the old one. The bug is using both.
The boring rule
- Slice and map parameters are shared storage unless you copy.
s2 := scopies the header.copy(orappendonto a fresh slice) copies elements.- After
s = append(s, x), treatsas the only header you still use. - Name functions after mutation:
markPaid,closeTable. If the name ispreview, copy inside. - Do not clone maps and slices “just in case” on every call. Clone at the boundary where ownership splits.
Try this
- In
mark_paid.go, replacecents[0] = 0withcents = append(cents, 0)and printorderinmain. Length inmaindoes not change (the header inmainwas not updated). - In
mark_paid_copy.go, writesafe := orderinstead ofmake+copy. Confirmorderis mutated again. - In
close_table.go, addopen[4] = "Dana"insidecloseTable. Confirmmainsees Dana. - Change
add_item.gosoorderis built with[]int{450, 120}(length 2, capacity 2).appendmust allocate. See whethernext[0] = 1still changesorder.