Go Call by Value — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code involving call-by-value semantics. Identify the bug, explain why, and provide the corrected code. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Mutation Doesn't Persist¶
package main
import "fmt"
type User struct {
Age int
}
func birthday(u User) {
u.Age++
}
func main() {
u := User{Age: 30}
birthday(u)
fmt.Println(u.Age) // expected 31
}
What's the bug?
Solution
**Bug**: `birthday` takes `User` by value. The function operates on a copy. The caller's `u` is unchanged. Output: **Fix** — pass a pointer: **Key lesson**: Go passes by value. To mutate, pass a pointer.Bug 2 🟢 — Slice Append Doesn't Propagate¶
package main
import "fmt"
func add(s []int, v int) {
s = append(s, v)
}
func main() {
s := []int{1, 2, 3}
add(s, 99)
fmt.Println(s) // expected [1 2 3 99]
}
Solution
**Bug**: `s = append(s, v)` modifies the LOCAL `s`. The caller's slice header is unchanged. Output: **Fix** (option A — return the new slice): **Fix** (option B — pointer to slice): **Key lesson**: Slice header reassignment is local. Return the new slice or pass `*[]T`.Bug 3 🟢 — Storing Slice Without Defensive Copy¶
package main
import "fmt"
type Cache struct {
items []int
}
func (c *Cache) Set(items []int) {
c.items = items // BUG: aliases caller's slice
}
func main() {
c := &Cache{}
src := []int{1, 2, 3}
c.Set(src)
src[0] = 999
fmt.Println(c.items) // expected [1 2 3]
}
Solution
**Bug**: `c.items = items` stores a slice that aliases the caller's backing array. When the caller mutates `src[0]`, the cache reflects it. Output: **Fix** — defensive copy: Now `c.items` has its own backing array. **Key lesson**: When a function stores caller-provided slices/maps, defensively copy to prevent caller-side mutation from corrupting the storage.Bug 4 🟢 — Nil Map Write Panic¶
Solution
**Bug**: `var m map[string]int` declares a NIL map. Writing to a nil map panics: `assignment to entry in nil map`. **Fix** — initialize: Or use a literal: **Key lesson**: Nil maps allow reads (return zero value) but panic on writes. Always initialize before writing.Bug 5 🟡 — Method With Value Receiver Doesn't Mutate¶
package main
import "fmt"
type Counter struct {
n int
}
func (c Counter) Inc() {
c.n++
}
func main() {
c := Counter{}
c.Inc(); c.Inc(); c.Inc()
fmt.Println(c.n) // expected 3
}
Solution
**Bug**: `Inc` has a value receiver. Each call operates on a COPY; caller's `c.n` never changes. Output: **Fix** — use pointer receiver: Now `c.Inc()` (with `c` a value) automatically takes the address; mutations persist. **Key lesson**: Value receivers operate on copies; for mutation use pointer receivers.Bug 6 🟡 — Pointer Receiver on Non-Addressable Value¶
package main
type T struct{ n int }
func (t *T) Inc() { t.n++ }
func main() {
// Method call on map value (not addressable)
m := map[string]T{"a": {n: 1}}
// m["a"].Inc() // compile error
_ = m
}
Solution
**Bug** (commented in starter): `m["a"]` is not addressable; you cannot call a pointer-receiver method on it. **Compile error**: `cannot call pointer method on m["a"]`. **Fix** — extract, mutate, re-store: Or store pointers in the map: **Key lesson**: Map values are not addressable. To mutate a struct stored in a map, either re-store after mutation or store pointers.Bug 7 🟡 — Loop Variable Address¶
package main
import "fmt"
func main() {
items := []int{1, 2, 3}
var ptrs []*int
for _, x := range items {
ptrs = append(ptrs, &x)
}
for _, p := range ptrs {
fmt.Println(*p)
}
}
In Go ≤ 1.21, what's the bug? In 1.22+?
Solution
**Pre Go 1.22**: `x` is the same variable across all iterations. All `&x` are the same pointer. After the loop, x = 3 (final value). Output: **Go 1.22+**: each iteration's `x` is a fresh variable. `&x` differs per iteration. Output: **Fix for pre-1.22**: Or pass through a function: **Key lesson**: Pre-1.22 loop variables are shared. Taking `&x` of a loop variable in the buggy way gives the same pointer N times. Go 1.22 fixes this.Bug 8 🟡 — Method Value Captures Stale Receiver¶
package main
import "fmt"
type S struct{ v int }
func (s S) Show() { fmt.Println(s.v) }
func main() {
s := S{v: 1}
show := s.Show
s.v = 99
show() // expected 99
}
Solution
**Bug**: Method value with VALUE receiver captures a COPY of `s` at binding time (when `v == 1`). Subsequent mutations don't affect the captured copy. Output: **Fix** (option A — pointer receiver): **Fix** (option B — call the method directly): **Key lesson**: Method values bound to value receivers freeze a snapshot. Use pointer receivers for live updates.Bug 9 🟡 — Returning Sub-slice Pins Large Array¶
package main
import "fmt"
func first(big []byte) []byte {
return big[:10]
}
func main() {
big := make([]byte, 1<<20) // 1 MB
first10 := first(big)
big = nil // try to release big
// first10 keeps the 1 MB array alive
fmt.Println(len(first10), cap(first10))
}
Solution
**Bug**: `first` returns a sub-slice that shares the backing array with `big`. As long as `first10` exists, the entire 1 MB array stays alive — even though we only use 10 bytes. Setting `big = nil` doesn't help; `first10` still references the array. **Fix** — copy out the bytes: Now `first10` has its own 10-byte backing; `big`'s array is collectable. **Key lesson**: Sub-slices keep the entire backing array alive. For long-term storage of small portions, copy out explicitly.Bug 10 🔴 — Race on Captured Slice¶
package main
import (
"fmt"
"sync"
)
func main() {
s := []int{1, 2, 3}
var wg sync.WaitGroup
for i := range s {
wg.Add(1)
go func() {
defer wg.Done()
s[i] *= 2 // BUG?
}()
}
wg.Wait()
fmt.Println(s)
}
Solution
**Bugs** (pre-1.22): 1. `i` is shared across goroutines — they may all read the same final `i`. 2. Even with that fixed, concurrent writes to different elements of `s` are SAFE (different memory locations), but if the slice were resized, races could appear. For pre-1.22, the iteration `i` issue is the main bug. Each goroutine sees `i = 3` (out of bounds) → panic. **Fix** (option A — pass i as arg): **Fix** (option B — Go 1.22+, no fix needed): Each iteration's `i` is per-iteration; no race on i. **Note**: Concurrent writes to different elements of a slice (different indices) are NOT a data race in Go's memory model. Reading and writing the same index would be. **Key lesson**: Loop-variable capture interacts with goroutines. Fix with shadowing, arg-passing, or Go 1.22.Bug 11 🔴 — Struct Returned by Value Allocates¶
package main
import "fmt"
type State struct {
Data [256]int
}
func newState() State {
return State{}
}
func main() {
var states []State
for i := 0; i < 100000; i++ {
states = append(states, newState())
}
fmt.Println(len(states))
}
What's the cost?
Solution
**Discussion**: `newState()` returns a 2 KB struct by value. The compiler may pre-allocate space in the caller for the return; the call writes into that space. For `append(states, newState())`: - Each call returns 2 KB on the stack. - `append` copies the 2 KB into the slice's backing array. - Total memory traffic: 100k × 2 KB × 2 (return + append) = 400 MB. **Optimization 1** — pre-allocate slice capacity: This avoids slice growth reallocations. **Optimization 2** — fill in place: This avoids the return-value copy. **Benchmark** (100k iterations): - Naive append: ~100 ms - Pre-allocated cap: ~50 ms - Fill in place: ~20 ms **Key lesson**: Large struct returns + slice append pay double for memory traffic. Pre-allocate or fill in place for hot paths.Bug 12 🔴 — Mutating Map Value Field¶
package main
import "fmt"
type Stats struct {
Count int
}
func main() {
m := map[string]Stats{"a": {Count: 1}}
// m["a"].Count++ // compile error
_ = m
}
What's the issue?
Solution
**Bug** (commented): `m["a"]` returns a COPY of the Stats value. You cannot modify a field of a copy through the map index expression. **Compile error**: `cannot assign to struct field m["a"].Count in map`. **Fix** (option A — extract, mutate, re-store): **Fix** (option B — store pointers in the map): **Key lesson**: Map values are not addressable. Either store pointers, or extract-mutate-restore.Bug 13 🔴 — Channel Direction Conversion¶
package main
import "fmt"
func send(ch chan<- int, v int) { ch <- v }
func recv(ch <-chan int) int { return <-ch }
func main() {
ch := make(chan int, 1)
go send(ch, 42)
fmt.Println(recv(ch))
// Now try to convert back
var bidi chan int = ch // OK
var sendOnly chan<- int = ch // OK
var recvOnly <-chan int = ch // OK
var bidi2 chan int = sendOnly // ?
}
Solution
**Bug**: `var bidi2 chan int = sendOnly` is a **compile error**. Once a channel is converted to a directional type (`chan<-` or `<-chan`), it cannot be converted back to bidirectional. **Fix**: keep the bidirectional reference somewhere: **Key lesson**: Channel direction is a one-way conversion. Hold onto the bidirectional reference if you need both directions later.Bonus Bug 🔴 — Storing Pointer to Local¶
package main
import "fmt"
type Manager struct {
last *int
}
func (m *Manager) Track(values []int) {
for _, v := range values {
m.last = &v // BUG?
}
}
func main() {
m := &Manager{}
m.Track([]int{1, 2, 3})
fmt.Println(*m.last)
}
In Go 1.21 vs 1.22, what does this print?