How do you correctly allocate a 2D slice, and what are the performance trade-offs?
Question 52MediumGo 1.22 to 1.25
make([][]int, n) creates only the outer slice of n nil rows. Writing grid[0][0] panics until you allocate each row. There are two approaches.
// 1) Row-per-allocation: simple, rows can be resized independently
grid := make([][]int, rows)
for i := range grid {
grid[i] = make([]int, cols)
}
// 2) Single backing allocation: 2 allocs total, contiguous, cache-friendly
backing := make([]int, rows*cols)
grid2 := make([][]int, rows)
for i := range grid2 {
grid2[i] = backing[i*cols : (i+1)*cols : (i+1)*cols] // cap-limit each row!
}
// 3) Fixed size known at compile time: an array of arrays, zero heap allocs possible
var board [8][8]byte
In option 2 the three-index slice matters. Without it, append(grid2[0], x) would overwrite the first element of row 1.
Trade-offs:
- Per-row allocation causes
rows+1allocations, more GC pressure, and poor locality. - A flat
[]intindexed byr*cols+cis fastest, and is what numeric libraries (gonum) use. - Arrays of arrays are values: copying
boardcopies all 64 bytes, which is handy for game-state snapshots in search algorithms.
More on Arrays, Slices, Maps & Strings
- Q50How do you delete elements from a slice efficiently, and what changed with slices.Delete in Go 1.22?
- Q51Why can't you compare slices with ==? How do you check equality of slices, maps, and structs containing them?
- Q53Why do the slices package functions use the constraint [S ~[]E, E any] instead of just []E?
- Q54Which slices and maps package functions do you use daily, and how do they work with iterators (Go 1.23)?
- Q55Describe how Go maps were implemented before Go 1.24 (buckets, tophash, load factor, growth).
- Q56What changed in Go 1.24 with Swiss Tables? Why are they faster?