What is a slice header, and what really gets passed when you pass a slice to a function?
A slice is a small struct of three words, {ptr *T, len int, cap int}. It holds a pointer to a backing array, the number of elements you can see (len), and how many elements exist from ptr to the end of that array (cap). Go passes everything by value, so a function gets a copy of the header. Both headers point at the same backing array.
So the callee can change existing elements and the caller sees it. If the callee changes its own len/cap (with append or reslicing), the caller does not see that, because the caller's header never changed.
func mutate(s []int) {
s[0] = 99 // visible to caller (shared array)
s = append(s, 4) // changes only the local header
}
func main() {
s := []int{1, 2, 3}
mutate(s)
fmt.Println(s) // [99 2 3]
}
What the interviewer wants to hear: slices are not "reference types" the way Java lists are. They are value headers that point to shared storage. That is why idiomatic APIs return the new slice (s = append(s, x)), and why you pass *[]T only when a function has to resize the caller's slice in place.
More on Arrays, Slices, Maps & Strings
- Q41What does this program print? (append inside a function with spare capacity)
- Q42How does append grow a slice? Why is the resulting capacity sometimes not exactly double?
- Q43What does this print? (two appends on the same base slice)
- Q44What is the full slice expression a[low:high:max], and when would you use it?
- Q45What is the difference between a nil slice and an empty slice? When does it matter?
- Q46Explain the semantics of the built-in copy. Is it safe with overlapping slices?