Mutability and Data Sharing
Mutability and Data Sharing
Overview
Understanding when data is copied vs shared is crucial for avoiding bugs and writing efficient Go code.
Value vs Reference Semantics
Value Types (Copied)
// Integers, floats, bools, strings, arrays, structs
a := 1
b := a
b = 2
fmt.Println(a) // 1 (unchanged)
arr := [3]int{1, 2, 3}
arr2 := arr
arr2[0] = 100
fmt.Println(arr[0]) // 1 (unchanged)Controlling Mutability
Immutable by Design
// Return new instead of modifying
func addItem(items []string, item string) []string {
return append(items, item) // May return new slice
}Defensive Copy
func process(s []int) {
local := make([]int, len(s))
copy(local, s) // Safe to modify local
}Deep Copy
type User struct {
Name string
Friends []string
}
func (u User) Clone() User {
friends := make([]string, len(u.Friends))
copy(friends, u.Friends)
return User{Name: u.Name, Friends: friends}
}Function Parameters
// Value: caller's data safe
func process(u User) {
u.Name = "modified" // Doesn't affect caller
}
// Pointer: can modify caller's data
func process(u *User) {
u.Name = "modified" // Affects caller
}Struct Field Mutability
type Counter struct {
count int
}
// Value receiver: cannot modify
func (c Counter) Increment() {
c.count++ // Modifies copy, original unchanged
}
// Pointer receiver: modifies original
func (c *Counter) Increment() {
c.count++ // Original modified
}Slice Gotchas
// Append may or may not share backing array
s := []int{1, 2, 3}
s2 := s[:2] // Shares backing array
s2 = append(s2, 4) // Might overwrite s[2]!
// Safer: force new allocation
s2 := append([]int(nil), s[:2]...)Summary
| Type | Behavior | Copy When… |
|---|---|---|
| int, bool, string | Value | Always copied |
| struct | Value | Always copied |
| array | Value | Always copied |
| slice | Reference | Use copy() |
| map | Reference | Rebuild manually |
| pointer | Reference | - |
More examples
Example: copy to isolate mutation
Save as main.go and go run . (with go mod init example if needed).
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a)
b[0] = 99
fmt.Println("a:", a)
fmt.Println("b:", b)
}Expected:
a: [1 2 3]
b: [99 2 3]
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import "fmt"
type User struct {
Name string
Friends []string
}
func (u User) Clone() User {
friends := make([]string, len(u.Friends))
copy(friends, u.Friends)
return User{Name: u.Name, Friends: friends}
}
type Counter struct{ count int }
func (c Counter) IncValue() {
c.count++ // mutates a copy only
}
func (c *Counter) IncPointer() {
c.count++
}
func main() {
// Value types: assignment copies.
a := 1
b := a
b = 2
fmt.Println("ints a,b:", a, b)
arr := [3]int{1, 2, 3}
arr2 := arr
arr2[0] = 99
fmt.Println("arrays arr,arr2:", arr, arr2)
// Reference-like headers: slice assignment shares data.
s := []int{1, 2, 3}
s2 := s
s2[0] = 100
fmt.Println("shared slice s:", s)
// Defensive copy of the backing array.
safe := make([]int, len(s))
copy(safe, s)
safe[0] = 1
fmt.Println("after defensive copy, s still:", s, "safe:", safe)
// append may or may not reallocate — force a fresh backing array.
base := []int{1, 2, 3}
alias := base[:2]
alias = append(alias, 9) // often overwrites base[2]
fmt.Println("append into shared capacity base:", base)
fresh := append([]int(nil), base[:2]...)
fresh = append(fresh, 9)
fmt.Println("append into fresh copy base:", base, "fresh:", fresh)
u := User{Name: "Ada", Friends: []string{"Bob"}}
u2 := u.Clone()
u2.Friends[0] = "Carol"
fmt.Println("clone independent:", u.Friends, u2.Friends)
c := Counter{}
c.IncValue()
fmt.Println("after value Inc:", c.count)
c.IncPointer()
fmt.Println("after pointer Inc:", c.count)
}Expected output:
ints a,b: 1 2
arrays arr,arr2: [1 2 3] [99 2 3]
shared slice s: [100 2 3]
after defensive copy, s still: [100 2 3] safe: [1 2 3]
append into shared capacity base: [1 2 9]
append into fresh copy base: [1 2 9] fresh: [1 2 9]
clone independent: [Bob] [Carol]
after value Inc: 0
after pointer Inc: 1
What to notice: Arrays and structs copy deeply for their own fields, but nested slices still share unless you copy. Value receivers never update the caller’s Counter.
Try next: Map the same experiment with map[string]int (assignment shares); fix the shared-append case by capping first: base[:2:2].