Understanding Slice Internals and Argument Passing in Go
package main
import "fmt"
type sliceHeader struct {
Data uintptr
Len int
Cap int
}
func main() {
topics := []string{"docker", "kubernetes", "istio"}
modifyFirst(topics)
fmt.Println(topics)
}
func modifyFirst(items []string) {
items[0] = "terraform"
}
// Output: [terraform kubernetes istio]
A slice is passed by value, but its underlying representation causes it to behave like a reference type. This behavior stems from the slice data structure—a small header containing a pointer to a backing array, a length, and a capacity. Although the header is copied during a function call, the pointer still refers to the same underlying array.
The internal structure resembles:
type slice struct {
ptr unsafe.Pointer
len int
cap int
}
Example 1: Slicing and Capacity
func main() {
nums := []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
sub := nums[2:7]
fmt.Printf("sub:%v len:%v cap:%v\n", sub, len(sub), cap(sub))
}
// Output: sub:[13 14 15 16 17] len:5 cap:8
sub shares the underlying array starting at index 2. Length is 5, but the capacity extends to the end of nums, giving 8 elements.
Example 2: Mutation Without Capacity Growth
func main() {
base := make([]int, 1, 5)
fmt.Println("base:", base)
mutate(base)
fmt.Printf("base:%v len:%v\n", base, len(base))
}
func mutate(s []int) {
s = append(s, 66, 77)
s[0] = 111
fmt.Println("inside:", s)
}
Output:
base: [0]
inside: [111 66 77]
base:[111] len:1
The backing array is modified by both the first append and the index assignment. However, the original slice header still has length 1, so only base[0] reflects the change.
Example 3: Capacity Exhaustion Triggers a New Backing Array
func main() {
original := make([]int, 1, 3)
fmt.Println("original:", original)
expand(original)
fmt.Printf("original:%v len:%v\n", original, len(original))
}
func expand(s []int) {
s = append(s, 2, 3, 4, 5) // exceeds capacity
s[0] = 99
fmt.Println("inside:", s)
}
Output:
original: [0]
inside: [99 2 3 4 5]
original:[0] len:1
When append exceeds the existing capcaity, a new larger array is allocated. The s header inside expand points to this new array, so modifications no longer affect original.
Passing a slice to a function never requires an explicit pointer; the header copy still references the same underlying data until a capacity-triggered allocation breaks the link.