How do variadic functions work? What is the aliasing trap when passing s... to a function that modifies or appends to its argument?
Inside the function, a variadic parameter xs ...int is a []int. A call like f(1, 2, 3) allocates a new slice. A call like f(s...) passes the caller's slice header as is: nothing is copied, so the function shares the caller's backing array.
func incr(xs ...int) {
for i := range xs {
xs[i]++
}
}
func appendX(xs ...int) []int { return append(xs, 99) }
func main() {
s := []int{1, 2, 3}
incr(s...)
fmt.Println(s) // [2 3 4]: caller's data mutated
t := make([]int, 3, 10)
_ = appendX(t[:2]...)
fmt.Println(t) // [0 0 99]: append wrote into spare capacity, overwriting t[2]
}
Why it prints that: t[:2] has len 2 and cap 10. append sees free capacity and writes 99 at index 2 of the shared array, which is also t[2]. This is a classic bug when helpers like append(prefix, extra...) are called with a slice that is reused by the caller. Defenses: copy inside the function (slices.Clone(xs)). Or cap the slice at the call site with a full slice expression, t[:2:2], so append must reallocate. Or document that the function takes ownership. Other points: you can't mix values and a spread (f(1, s...) is a compile error), and f() passes a nil slice.
More on Go Idioms, Design Patterns & Language Design
- Q549How do you implement a thread-safe singleton or lazy initialization in Go? Compare sync.Once, sync.OnceValue and package-level init.
- Q550How do you model enums in Go with iota? How do you add String() via go generate stringer, validate values, and handle the zero value?
- Q552Explain closures in Go: what they capture, when captured variables escape to the heap, and how to build a generator or middleware with them.
- Q553Why does Go have no inheritance, function overloading, default arguments, ternary operator or exceptions? How do idiomatic Go programs work around each?
- Q554Can you compare functions in Go? Why is a func only comparable to nil, and how does that affect using funcs as map keys?
- Q555Does Go do tail-call optimization? What happens with deep recursion, and what does "goroutine stack exceeds 1000000000-byte limit" mean?