Growth is amortised constant but not free: each reallocation copies everything
appended so far, and the allocations are what the profiler shows.
func grow(n int) []int {
var s []int
for i := 0; i < n; i++ {
s = append(s, i)
}
return s
}
func sized(n int) []int {
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}
return s
}
BenchmarkGrow-8 18432 ns/op 357626 B/op 19 allocs/op
BenchmarkSized-8 6871 ns/op 81920 B/op 1 allocs/op
Read the allocation column rather than the time. Nineteen allocations to build
ten thousand elements, and the number is worth pausing on, because the answer
people give is "log2 n" and log2 of ten thousand is about 13.3. If the capacity
really doubled every time you would see thirteen or fourteen reallocations, not
nineteen. It does not: Go doubles only while the slice is small, and above a
threshold in the low hundreds of elements the growth factor tapers towards 1.25
so that large slices do not overshoot by half their own size. A gentler factor
means more steps to reach the same length, which is where the extra five or six
allocations come from.
The trade is deliberate and worth being able to state: doubling minimises the
number of reallocations and wastes up to half the memory, 1.25 growth wastes far
less and reallocates more often, and Go picks the first for small slices and
drifts to the second for large ones. Either way each reallocation copies
everything appended so far, so the total copying is still linear in n. The bytes
figure, 357,626 against a final 81,920, is more than four times the final size
because every abandoned array is still garbage the collector has to walk and
free.
The capacity you see is not always the one you asked for:
s := make([]int, 0, 5)
s = append(s, 1, 2, 3, 4, 5, 6)
fmt.Println(len(s), cap(s))
The runtime rounds the request up to an allocator size class, and the growth
factor is not a clean doubling for large slices — it tapers towards 1.25 above a
few hundred elements to avoid wasting memory. So cap after a growth is an
implementation output, never something to assert on.
The rule is simply to pass the length when you know it, which you usually do:
make([]T, 0, len(input)) before a transform loop, and the size hint on
make(map[K]V, n) for the same reason. The other half of the rule is not to
guess wildly — make([]byte, 0, 1<<20) for a response that is normally 200 bytes
moves the cost from the collector to the heap and keeps a megabyte alive for as
long as anything holds the slice.