Arrays
An array in Go is a fixed-length, ordered collection of elements that all share the same type. Once you declare an array with a given length, that length is permanently part of the array’s type — you cannot grow or shrink it. Arrays are the foundation that Go’s much more commonly used slice type is built on top of, so understanding how arrays behave under the hood explains a lot of slice behavior you’ll meet later, such as aliasing and reallocation during append.
Overview / How it works
An array is a contiguous block of memory that holds a fixed number of elements of a single type. When you write var arr [5]int, Go allocates space for exactly five int values, laid out back-to-back in memory. Accessing arr[i] is a constant-time operation: the runtime computes the memory address as base + i * elementSize and reads directly from it. There is no indirection, no pointer chasing — this is why arrays (and the slices built on them) are so fast to iterate.
Every element of a freshly declared array is set to the zero value of its element type: 0 for numeric types, "" for strings, false for booleans, nil for pointers, maps, slices, and interfaces, and a recursively zeroed struct for struct element types. There is no such thing as an “uninitialized” array in Go the way there is in C — every slot always holds a valid, well-defined value.
The most important thing to internalize about arrays is that the length is part of the type. [3]int and [5]int are two completely different, incompatible types — the compiler will not let you assign one to the other, pass one where the other is expected, or compare them with ==. This is different from languages where “array of int” is a single generic type regardless of size.
Arrays are also value types. Assigning an array to another variable, or passing one to a function, copies every element. This is the single biggest difference from slices, which are reference-like (a slice header pointing at shared underlying memory). If you want a function to mutate the caller’s array, you must pass a pointer to it (*[N]T) or use a slice instead.
Because arrays are value types with comparable elements (numbers, strings, booleans, other comparable arrays, or structs made of comparable fields), two arrays of the same type can be compared directly with == — this checks that every corresponding element is equal. Slices cannot be compared this way at all; comparing two slices with == is a compile error (except comparing a slice to the literal nil).
Go also supports multi-dimensional arrays, which are really just “arrays of arrays.” A type like [3][4]int is an array of three elements, where each element is itself an array of four ints — a 3×4 grid stored as one contiguous 12-element block of memory, not as a jagged collection of separately allocated rows.
In idiomatic Go, you will reach for slices far more often than arrays, because slices can grow. Arrays shine in narrower situations: when a fixed size is meaningful and part of the API contract — a SHA-256 hash is always exactly 32 bytes ([32]byte), a 3D coordinate is always exactly three floats, a tic-tac-toe board is always exactly nine cells. When the size might change, use a slice.
Syntax
There are three common ways to declare an array, plus the multi-dimensional form:
var arrayName [length]elementType
arrayName := [length]elementType{value1, value2, value3}
arrayName := [...]elementType{value1, value2, value3}
var grid [rows][cols]elementType
| Form | Meaning |
|---|---|
var arr [5]int |
Declares an array of exactly 5 ints, all zero-valued. |
arr := [5]int{1, 2, 3} |
A literal with an explicit length; any elements not listed are zero-valued. |
arr := [...]int{1, 2, 3} |
The ... tells the compiler to count the elements and set the length automatically — here, length 3. |
var grid [3][4]int |
A 3×4 two-dimensional array — an array of 3 arrays, each holding 4 ints. |
len(arr) |
Returns the array’s fixed length as an int. |
Examples
Example 1: Declaring and filling an array
package main
import "fmt"
func main() {
var scores [5]int
scores[0] = 90
scores[1] = 85
scores[2] = 78
scores[3] = 92
scores[4] = 88
fmt.Println(scores)
fmt.Println("Length:", len(scores))
}
Output:
[90 85 78 92 88]
Length: 5
The var scores [5]int declaration allocates five ints, all initially zero, and each is then assigned individually by index. Printing an array with fmt.Println shows its elements space-separated inside square brackets — this is Go’s default formatting for arrays and slices alike.
Example 2: Array literal with range and a running sum
package main
import "fmt"
func main() {
primes := [...]int{2, 3, 5, 7, 11}
sum := 0
for i, v := range primes {
fmt.Printf("index %d: %d\n", i, v)
sum += v
}
fmt.Println("Sum:", sum)
fmt.Println("Length:", len(primes))
}
Output:
index 0: 2
index 1: 3
index 2: 5
index 3: 7
index 4: 11
Sum: 28
Length: 5
The [...]int{...} form lets the compiler count the five elements for you, producing a [5]int. The for i, v := range primes loop yields each index and a copy of the corresponding value on every iteration — mutating v inside the loop would not affect primes.
Example 3: Value semantics — arrays copy, pointers don’t
package main
import "fmt"
func double(arr [3]int) {
for i := range arr {
arr[i] *= 2
}
}
func doubleInPlace(arr *[3]int) {
for i := range arr {
arr[i] *= 2
}
}
func main() {
nums := [3]int{1, 2, 3}
double(nums)
fmt.Println("After double (by value):", nums)
doubleInPlace(&nums)
fmt.Println("After doubleInPlace (by pointer):", nums)
}
Output:
After double (by value): [1 2 3]
After doubleInPlace (by pointer): [2 4 6]
Calling double(nums) passes a full copy of the array into the function; doubling the copy’s elements has no effect on nums back in main. Calling doubleInPlace(&nums) instead passes the memory address of nums. Go automatically dereferences a pointer to an array when you index it or range over it, so the loop mutates the original array directly, and the change is visible after the call returns.
How it works step by step
- Declaration:
var arr [5]intreserves five contiguousint-sized memory slots and zero-fills them — no separate allocation step is needed, unlike a slice or map. - Indexing:
arr[i]computes a direct memory offset. Ifiis a constant known at compile time and it’s out of range, the compiler rejects the program outright; ifiis a variable, the runtime checks the bound on every access and panics with “index out of range” if it’s violated. - Assignment and passing: whenever an array is assigned to a new variable, stored in a struct field, or passed as a function argument, Go copies every element. For a large array this copy has a real cost — pass a pointer or use a slice to avoid it.
- Ranging:
for i, v := range arrwalks the indices in order, and on each iterationvis a fresh copy ofarr[i]— safe to read, but modifyingvnever touches the array. - Comparison:
arr1 == arr2is only legal when both operands are the exact same array type (same element type and same length); the compiler compares every element pairwise and the whole expression istrueonly if all of them match.
Common Mistakes
Mistake 1: Trying to append to an array
Coming from slices, it’s easy to reach for append on something that is actually a fixed array:
var arr [3]int
arr = append(arr, 4)
// compile error: first argument to append must be a slice; have arr (variable of type [3]int)
append only works on slices, because only a slice header can be replaced with a new, larger one. An array’s length is fixed by its type, so there’s nothing for append to grow. The fix is to convert the array to a slice first with a slice expression, then append to that slice:
package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
s := arr[:]
s = append(s, 4)
fmt.Println(s)
}
Output:
[1 2 3 4]
Here arr[:] creates a slice viewing all of arr, with length 3 and capacity 3. Since appending needs room beyond the existing capacity, Go allocates a brand-new backing array for s and copies the elements over — the original arr is left untouched.
Mistake 2: Assuming array length doesn’t matter for type compatibility
Because slices of any length share the same type []int, it’s tempting to assume arrays work the same way. They don’t — length is part of the type, so a function expecting [5]int flatly rejects a [3]int:
func printArr(a [5]int) {
fmt.Println(a)
}
func main() {
nums := [3]int{1, 2, 3}
printArr(nums)
// compile error: cannot use nums (variable of type [3]int) as [5]int value in argument to printArr
}
The idiomatic fix is usually to accept a slice instead of a fixed-size array, since slices of any length satisfy the same parameter type — and any array can be converted to a slice with arr[:]:
package main
import "fmt"
func printNums(nums []int) {
for _, n := range nums {
fmt.Println(n)
}
}
func main() {
a := [3]int{1, 2, 3}
b := [5]int{10, 20, 30, 40, 50}
printNums(a[:])
printNums(b[:])
}
Output:
1
2
3
10
20
30
40
50
By taking []int instead of a fixed-length array, printNums happily accepts slices derived from arrays of any size.
Best Practices
- Default to slices for general-purpose collections; reach for an array only when the fixed size is a meaningful, unchanging part of the data — hashes, coordinates, fixed-size buffers, lookup tables.
- Use
[...]T{...}when writing a literal so the compiler counts the elements for you, reducing the chance of an off-by-one length mistake. - Pass large arrays by pointer (
*[N]T) if you must pass one around and want to avoid copying, or better yet, use a slice, which is cheap to pass regardless of size. - Take advantage of array comparability (
==) when you need value-type equality — for example, comparing two fixed-size hash values directly, which slices cannot do. - Never index an array (or slice) with a value you haven’t validated is in range; let constant out-of-range indices be caught by the compiler, and guard variable indices with an explicit bounds check or a safe iteration pattern like
range. - Document why you chose a fixed-size array over a slice in a doc comment if it’s not obvious — future readers will otherwise wonder why the size can’t change.
Practice Exercises
- Declare a
[7]stringarray holding the names of the days of the week, fill it in, and usefor i, v := rangeto print each one as"day 0: Sunday","day 1: Monday", and so on. - Write a function
func sumArray(a [10]int) intthat returns the sum of all ten elements. Call it frommain, then try modifyingainside the function and confirm (by printing before and after the call) that the caller’s array is unaffected. - Declare two
[3]intarrays with identical values and compare them with==, printing the boolean result. Then try writing the equivalent comparison for two[]intslices and observe the compiler error — write down, in your own words, why arrays support this and slices don’t.
Summary
- An array is a fixed-length, contiguous block of memory holding elements of one type; its length is part of its type.
- Arrays are zero-valued on declaration — there is no uninitialized state.
[3]intand[5]intare different, incompatible types, so array length must match exactly for assignment, comparison, and function parameters.- Arrays are value types: assigning or passing one copies every element, unlike slices which share underlying memory.
- Use a pointer (
*[N]T) to let a function mutate the caller’s array in place. - Arrays of comparable element types can be compared directly with
==; slices cannot. - In practice, prefer slices for most collections and reserve arrays for cases where a fixed size is intrinsic to the data.
