skip to content

In Go, how does `[3][4]int` differ from `[][]int` for a two-dimensional grid?

level: middleimportance: nice to knowfreq 26%

answer

  1. count the allocations each form needs
  2. can two rows have different lengths
  3. what does make on the outer level give you
  4. one contiguous block versus scattered rows

basics

~20 s

[3][4]int is one value holding 12 ints contiguously, with both dimensions fixed by the type and the whole thing copied on assignment. [][]int is a slice of independent row slices: rows are allocated one at a time, may have different lengths, and are shared rather than copied.

solid answer

~50 s

`[3][4]int` is an array of arrays: a single value of 12 contiguous ints whose shape is fixed by the type, allocated as one block, and copied wholesale when you assign or pass it. `[][]int` is a slice whose elements are themselves slices, so `make([][]int, 3)` gives you three nil rows and you still need a loop to `make` each row — three more allocations, each row's elements living somewhere separate. That layout allows jagged rows of different lengths and lets rows be reassigned or appended to, at the price of an extra indirection per row and worse locality. When the shape is fixed and known, an array of arrays is the cheapest and clearest option. When it is not, a common middle ground is one flat `[]int` of `rows*cols` with the index computed as `r*cols + c`, which keeps the elements contiguous while letting the size be chosen at run time.

code

go · 8 lines
go
var grid [3][4]int // one contiguous block of twelve ints, already zeroed
grid[1][2] = 7

rows := make([][]int, 3) // three nil rows so far
for i := range rows {
	rows[i] = make([]int, 4) // each row gets its own array
}
rows[1][2] = 7

go deeper

for a junior

Remember that a slice of slices needs a loop to create each row, because make on the outer slice leaves the rows nil, and that indexing a nil row panics.

for a middle

Explain the layout difference: an array of arrays is one contiguous block with both lengths in the type, while a slice of slices is one allocation per row with an indirection on every access.

for a senior

Pick deliberately for the workload. Scans over a large grid want contiguous memory, so reach for a fixed array or a flat slice with computed indices, and reserve the slice of slices for genuinely jagged data.

for a principal

Treat the layout as an interface decision: exposing [][]int lets callers hold and mutate individual rows, which is hard to walk back later. Decide whether your package hands out a shape or a value.

## Three ways to spell a two-dimensional grid Go has no dedicated matrix type, so a grid is built out of the two composite types the language does have. The choice between them is a memory-layout choice. ### `[3][4]int` — an array of arrays This is a single value: three elements, each of which is a `[4]int`. Both dimensions come from the type, so the whole thing is 12 ints laid out contiguously in memory, in row-major order. It needs no `make`, it is zeroed on declaration, and a local one can live in the function's frame with no allocation at all. Because it is one value, all the array rules apply to the whole grid: assigning it copies all 12 ints, passing it to a function copies all 12, and its shape can never change. `len(grid)` is 3 and `len(grid[0])` is 4, both constants. ```go var grid [3][4]int grid[1][2] = 7 other := grid // copies all twelve ints ``` ### `[][]int` — a slice of slices Here the outer value is a slice whose element type is `[]int`. This is the flexible form, and the thing that surprises newcomers is that `make` only builds the outer level: ```go rows := make([][]int, 3) // three rows, each a nil []int rows[1][2] = 7 // panics: index out of range on a nil row ``` Each row has to be created in its own right: ```go rows := make([][]int, 3) for i := range rows { rows[i] = make([]int, 4) } ``` That is four allocations in total, and the four rows' elements are four separate runs of memory that happen to be pointed at by one outer slice. Consequences: - **Rows are independent.** They may have different lengths — a jagged table — and a row can be replaced or grown without touching the others. - **There is an extra indirection.** Reading `rows[r][c]` follows the outer slice to a row value and then that row to its elements, whereas `grid[r][c]` is one address computation. - **Locality is worse.** Separately allocated rows need not be adjacent, so a scan over a large grid touches scattered memory. - **Copying the outer slice shares the rows.** Assigning `other := rows` gives two outer slices over the same row values, so writes through either are visible to both, all the way down. ### The flat slice When the dimensions are only known at run time but you still want one contiguous block, the usual answer is to flatten: ```go cols := 4 cells := make([]int, 3*cols) cells[1*cols+2] = 7 ``` One allocation, contiguous elements, run-time size — at the price of doing the index arithmetic yourself and losing the `grid[r][c]` notation. A variation keeps both: allocate one flat backing slice, then build a `[][]int` whose rows are sub-slices of it, giving two-index syntax over contiguous memory. ### Choosing Fixed, small, shape known at compile time — an array of arrays, and let it be copied. Shape known only at run time, or rows genuinely of different lengths — a slice of slices. Shape known only at run time but performance-sensitive and rectangular — a flat slice, possibly with row sub-slices layered over it. The same reasoning applies to the fixed-size buffers a device-facing daemon holds: a ring of eight 32-byte frames is naturally `[8][32]byte`, one contiguous 256-byte value with no allocation and no per-row pointer, and it can be handed around as a single value.

  • Why does `rows := make([][]int, 3); rows[0][0] = 1` panic?
    `make([][]int, 3)` builds only the outer slice; its three elements are the zero value of `[]int`, which is a nil slice of length 0. Indexing element 0 of a length-0 row is out of range, so it panics. Each row needs its own `make` before it can be indexed.
  • How do you get two-index syntax over one contiguous allocation?
    Allocate a single flat backing slice of `rows*cols` elements, then build the `[][]int` by slicing that backing array into rows: each row is `flat[i*cols : (i+1)*cols]`. You get `g[r][c]` notation, one allocation, and contiguous memory — at the cost of rows that must not be individually regrown.

saying these in an interview costs you the question

  • Thinks make([][]int, 3) allocates the rows too
  • Assumes slice-of-slices rows are contiguous in memory
  • Says [3][4]int can hold rows of different lengths
  • Believes copying a [][]int deep-copies the rows