Last Updated: May 22, 2026
An array in Go is a fixed-size sequence of elements that all share the same type. The size is part of the type itself, so [5]int and [10]int are two distinct types that aren't interchangeable. Arrays are the foundation slices are built on, but in everyday Go code you'll use them directly only when the size is genuinely fixed, like a 16-byte cryptographic block or a lookup table indexed by day of the week. This lesson covers declaration, indexing, value semantics, equality, multi-dimensional layouts, and when to use an array instead of a slice.
An array type is written [N]T, where N is a non-negative integer constant and T is the element type. The declaration creates space for exactly N values of type T, all initialized to the zero value.
The variable topProducts is an array of three strings. Because we didn't supply any values, each slot holds the zero value for string, which is the empty string "". The printed form shows three empty positions between the brackets, separated by spaces.
You assign values by index, the same way you'd write to any indexed variable.
Indices run from 0 to N-1. Writing to index 3 on a [3]string is a compile-time error if the index is a constant, and a runtime panic (index out of range [3] with length 3) if it's computed at runtime. Go does the bounds check for you, so an out-of-range access never silently corrupts memory the way it can in C.
The size in the type has to be a constant the compiler can evaluate, not a runtime value. var items [n]int where n is a variable does not compile. If you need a size that's only known at runtime, you want a slice.
lenYou read an array element with the same [] syntax used to write one. The expression array[i] evaluates to the value at index i.
The built-in len function returns the number of elements in the array. For arrays, len is a compile-time constant, because the size is baked into the type. The compiler can evaluate len(stock) to 4 without running the program. This is one of the few places in Go where a function-looking expression isn't a runtime call.
Cost: Indexing an array is O(1) and never allocates. The bounds check is one comparison the compiler often elides when it can prove the index is in range (for example, inside for i := 0; i < len(stock); i++).
Go also defines cap for arrays. For a plain array value, cap(arr) equals len(arr), since the size is fixed and can't grow. The two functions diverge only for slices, where capacity is a separate concept.
Iterating an array uses the same for and range forms as any other sequence.
range over an array yields (index, value) pairs in order. The value is a copy of the element, not a reference into the array, so writing to r inside the loop wouldn't change the array. We'll come back to this when we talk about value semantics.
Every Go variable is initialized to its type's zero value. For an array, the zero value is an array of the right size with every element set to the element type's zero value. There's no concept of an "uninitialized" array slot.
This is genuinely useful. A [7]int for the number of orders per day starts as seven zeros without any extra setup, and you can immediately do weekly[2]++ without first writing a loop to clear it. Compare that to languages where reading an uninitialized array gives back whatever was previously in that memory, and the cost of zero-initialization buys you real safety.
The zero value rule also applies to arrays of structs. Each element of var customers [3]Customer is a Customer with every field set to its own zero value.
[...]TAn array literal lets you create an array with its values already filled in, in one expression.
The size in the brackets has to match the number of values. Writing [3]float64{19.99, 7.50, 4.25, 29.00} is a compile error because four values can't fit in a three-element array. If you provide fewer values than the size, the missing positions stay at the zero value.
When the size matches the number of literal values exactly, retyping the count is busywork. Go offers [...]T to let the compiler count the elements for you.
The ... doesn't mean "variable size". The compiler still produces a fixed-size array, but it counts the literal values and substitutes the count into the type. Add or remove an element and the type changes accordingly. This is the form you'll see most often when writing an array literal by hand.
You can also initialize specific indices with the index: value form, which is handy when most of the array is zero.
Index 0 is 12, index 6 is 20, and the rest fall back to the zero value. The indices must be constants and must fit in the declared size.
Go arrays differ from many other languages: in Go, an array is a value, not a reference. In Go, an array is a value. Assigning one array to another copies all the elements, and passing an array to a function does the same.
copyOf := original makes a fresh array of three integers and copies each element across. Writing to copyOf[0] modifies only the new array, and original is untouched. The two variables hold independent data, even though they were initialized from the same source.
The same rule applies to function calls. The parameter inside the function is a copy of the array the caller passed in.
The function modified its parameter, but the caller's weekly is unchanged. This is the same pass-by-value rule we saw with int and string parameters, applied uniformly to arrays.
Cost: Passing a [1024]int by value copies 1024 integers (8 KB on a 64-bit machine) on every call. For large arrays in hot paths, pass a pointer (*[1024]int) or use a slice.
Returning an array works the same way. The returned array is a copy, which is fine for small fixed-size data and a performance concern for large ones.
For the seven-element array above, the copy is negligible. For something like a 4 KB image tile, it adds up.
Two arrays of the same type are comparable with == and != whenever their element type is comparable. The comparison checks every element pairwise and returns true only if all elements match.
The comparison is element-wise, so a == b is true even though the two arrays live at different memory locations. This is different from how comparison works for slices, which aren't comparable with == at all (except against nil).
The element type matters. Most built-in types (int, string, float64, bool, pointers, channels, comparable structs) are comparable, so arrays of those types are also comparable. Slices, maps, and functions are not comparable, so arrays containing them aren't either.
Trying to compare these two arrays with == is a compile error, because the element type []int isn't comparable. The example above doesn't include the comparison so it runs and prints both arrays, but uncommenting a == b would reproduce the error.
Arrays of different sizes have different types, so they can't be compared at all. [3]int and [4]int are unrelated types, and trying to write [3]int{} == [4]int{} is a compile error, not a runtime false.
Cost: Array equality runs in O(N) where N is the length. It's still useful for small arrays used as map keys or set members, where the fixed size keeps the work predictable.
A direct consequence of array equality is that an array of comparable elements can be used as a map key. A slice can't, because slices aren't comparable. This is one of the few situations where an array is genuinely the better choice over a slice.
The map key here is a [2]int array, which is fine because arrays of int are comparable. Trying to use []int as a key would be a compile error.
An array of arrays is written by stacking the size prefixes. [3][4]int is "an array of 3 elements, each of which is an array of 4 ints." Reading right to left helps: the innermost type is the element, the outermost is the container.
Each ratings[i] is itself an array of four int values, so you can assign to it as a whole or index into it with a second [].
ratings[1] selects the second row, and [2] then picks the third element of that row. The memory layout is contiguous: the compiler lays out all 12 integers in a single block, row by row. That's different from a slice of slices, where each inner slice is a separate heap allocation pointed to by the outer slice.
The diagram below shows how a [3][4]int sits in memory as one flat block, indexed by row and column.
A [3][4]int and a [][]int look similar in source code but behave very differently. The fixed array stores its data inline, with no extra indirection, and it's cheap to copy as a whole (12 integers in this case). It's not resizable, and passing one by value copies all 12 integers each time. The slice version supports rows of different lengths and runtime growth, at the cost of an extra pointer dereference per access.
Higher dimensions stack the same way. [2][3][4]int is two arrays of [3][4]int, and so on. In practice you rarely need more than two dimensions in real Go code.
Slices are the default. Most Go code uses []T for any list of values, because slices grow, shrink, and support append. Arrays are appropriate in a specific set of situations where their fixed size and value semantics are an advantage rather than a constraint.
The table below summarizes the trade-offs.
| Property | Array [N]T | Slice []T |
|---|---|---|
| Size in type | Yes, part of the type | No, runtime length |
| Resizable | No | Yes, via append |
| Pass to function | Copies all elements | Copies a small header (24 bytes on 64-bit), shares the underlying data |
Equality with == | Yes, if elements are comparable | Only against nil |
| Usable as map key | Yes, if elements are comparable | No |
| Zero value | Array with every element zero | nil slice (length 0, no backing array) |
| Typical use | Fixed-size data, lookup tables, hashing | Almost everything else |
Use an array when:
[32]byte is the right type for it. A day-of-week table has exactly 7 entries. RGB color is exactly 3 channels.[2]int is a valid key while []int isn't.func md5sum(data []byte) [16]byte tells the caller exactly how many bytes come back.Use a slice when the size isn't known up front, when the data needs to grow, or when you want efficient pass-by-reference semantics.
A common pattern in production Go is to use arrays as the underlying storage and hand out slices that reference them. A 512-byte stack-allocated buffer for parsing might look like var buf [512]byte, and the parser then works on buf[:n] as a slice. The array provides the memory, the slice provides the view, and there's no heap allocation. This pattern shows up throughout the standard library, especially in bufio, crypto, and the encoding packages.