Background:
Go was designed as a language with explicit value semantics: almost everything is copied by value when passed, including structs, but not pointers and slices. This simplifies reasoning and enhances safety, but introduces several "traps".
Issue:
Developers often expect that modifications to a struct passed to a function will be visible externally. However, the entire content (including nested fields — by value!) is copied. Slices and maps have different behavior, where the "container" is copied, but not the "content".
Solution:
Pass large structs by pointer if modification is expected. For slices, only the descriptor (length, capacity, pointer) is copied, not the content — changes to the original slice (via index) will be visible externally. For structs — everything is copied:
type Point struct { X, Y int } func move(p Point) { p.X = 100 } func movePtr(p *Point) { p.X = 100 } func demo() { pt := Point{10, 10} move(pt) fmt.Println(pt.X) // 10 movePtr(&pt) fmt.Println(pt.X) // 100 }
Key Features:
If a struct is changed inside a function, will the original change?
No, if the struct is passed by value — changes are local.
type User struct {Name string} func f(u User) {u.Name = "Ann"}
If an element of a slice is changed inside a function, will the original change?
Yes. Slices are a "view" on a shared array. Changing an element modifies the original data.
func f(s []int) {s[0] = 99}
What happens if a slice created inside a function is returned?
The "header" of the slice is copied, but the underlying array remains accessible. If no reference is saved externally, the data may be collected by GC.
In a function, processing the User struct was done by value — changes did not propagate back, making bugs hard to trace.
Pros:
Cons:
Large structs are explicitly passed by pointer, and behavior regarding slices is always commented or checked. There is no confusion, everyone expects value semantics.
Pros:
Cons: