"A slice is not the data. It's a window into the data."
A slice in Go is a 24-byte struct (on 64-bit systems) containing metadata:
// Runtime definition (simplified)
type slice struct {
ptr unsafe.Pointer // Points to underlying array
len int // Current length
cap int // Capacity
}
Slice Header (24 bytes):
┌─────────────┬─────┬─────┐
│ ptr │ len │ cap │
│ 0x1400... │ 3 │ 3 │
└──────┬──────┴─────┴─────┘
│
└──→ Underlying array: [1, 2, 3]
When you pass a slice to a function, Go copies the header, not the data:
func modify(s []int) {
s[0] = 999 // ✓ Modifies underlying array
}
func main() {
nums := []int{1, 2, 3}
modify(nums) // Copies header (24 bytes)
fmt.Println(nums) // [999, 2, 3]
}
Both headers point to the same underlying array.
When capacity is exceeded, append allocates a new array:
func appendNoPtr(s []int) {
s = append(s, 4)
}
func main() {
nums := []int{1, 2, 3} // cap = 3
appendNoPtr(nums)
fmt.Println(nums) // [1, 2, 3] — unchanged!
}
What happens:
1. main():
nums = {ptr: 0x1400, len: 3, cap: 3}
2. appendNoPtr receives COPY:
s = {ptr: 0x1400, len: 3, cap: 3}
3. append reallocates:
s = {ptr: 0x1580 → [1,2,3,4], len: 4, cap: 6}
↑ NEW array!
4. Back in main():
nums = {ptr: 0x1400 → [1,2,3], len: 3, cap: 3}
↑ Still points to OLD array