Why does preallocating slices and maps matter? What does this print?
func main() {
s := make([]int, 0, 2)
a := append(s, 1)
b := append(s, 2)
fmt.Println(a[0], b[0], len(s), cap(s))
var grow []int
prev := -1
for i := range 2000 {
grow = append(grow, i)
if cap(grow) != prev {
prev = cap(grow)
}
}
_ = prev
}
Prints 2 2 0 2. a and b share s's backing array because it had spare capacity; the second append overwrote index 0. This aliasing is a classic bug when you append to a slice you don't own — use slices.Clip(s) or a full slice expression s[:len(s):len(s)] to force a copy on append.
Performance side: appending to an unsized slice reallocates and copies whenever capacity is exceeded — doubling below 256 elements, then a smooth transition toward ~1.25x growth (Go 1.18+ formula newcap += (newcap + 3*256) / 4), with the result rounded up to a malloc size class. For n known up front:
out := make([]Result, 0, len(inputs)) // one allocation
for _, in := range inputs {
out = append(out, process(in))
}
idx := make(map[string]int, len(inputs)) // avoids incremental rehash/growth
Maps are worse: growth means rehashing entries into new buckets (Go 1.24's Swiss-table maps split into tables of at most 1024 slots and grow one table at a time, which bounds the pause, but presizing still avoids all of that work). Gotcha: make([]T, n) vs make([]T, 0, n) — using the former then append leaves n zero values at the front. Also, maps never shrink: deleting all keys keeps the bucket memory; recreate the map to release it.
More on Performance, Profiling & Testing
- Q393What does this program do?
- Q394How do GOGC and GOMEMLIMIT interact, and how would you tune the GC for a latency-sensitive service?
- Q396What is false sharing and how would you detect and fix it in Go?
- Q397How does struct field ordering affect memory usage and performance?
- Q398How can you convert between []byte and string without allocating? When is it safe?
- Q399How does the race detector work, what does it cost, and what are its limits?