Fixed-size arrays and the workhorse slice type.
Slice Internals (Length and Capacity)
Understanding the relationship between len, cap, and the underlying array
makes a lot of Go idioms make sense.
A slice is a header
You can picture a slice as three fields:
┌─ slice s
│ ptr ──────► [ _ , _ , _ , _ , _ , _ , _ ] // backing array
│ len = 3
│ cap = 7
└──
len(s)— how many elements you can actually read withs[i].cap(s)— how many slots the backing array has starting from the slice's first element.- When
appendruns out of capacity, it allocates a bigger array and copies.
You'll see capacity stay at 4 until it overflows, then double (in this implementation; the growth factor isn't part of the language spec).
s[low:high:max] — capping a slice
The three-index slice expression lets you control the capacity of the result:
s := []int{0, 1, 2, 3, 4}
view := s[1:3:3] // len=2, cap=2
This is useful when you want to prevent later append calls from accidentally
clobbering elements past the end:
nil slice vs empty slice
var a []int // nil — ptr=nil, len=0, cap=0
b := []int{} // not nil, but len=0, cap=0
Both have len(a) == 0. You can append to either. The difference shows up in
comparisons (a == nil is true, b == nil is false) and when serializing to JSON
(nil → null; []int{} → []). Prefer nil unless you specifically need the
"empty but not nil" distinction.
Pre-allocate when you know the size
If you'll append n items, give make a capacity:
ids := make([]int, 0, len(users)) // pre-sized — no reallocations
for _, u := range users {
ids = append(ids, u.ID)
}
This avoids the doubling-and-copying that happens when capacity runs out — a free speedup whenever you know the size in advance.