Skip to content
CalliCoder

Golang Arrays Explained with Examples

Golang 11 min read

Why the length is part of the type, what a copy actually copies, the range loop that iterates a snapshot, and the handful of cases where an array beats a slice.

Most Go code uses slices, and most Go code is right to. Arrays exist underneath them, and the four properties that make arrays awkward as a general container are exactly what make them the correct choice in a few specific places.

Written against Go 1.22.

The length is part of the type

var a [3]int
b := [3]int{1, 2, 3}
c := [...]int{1, 2, 3, 4}     // compiler counts: [4]int
d := [5]int{2: 10}            // sparse: [0 0 10 0 0]

[3]int and [4]int are different types. They cannot be assigned to each other, and a function taking [3]int will not accept [4]int:

func sum(xs [3]int) int { ... }

sum([4]int{1, 2, 3, 4})    // compile error: cannot use [4]int as [3]int

This is the property that makes arrays impractical as a general parameter type, and it is why slices exist. A function taking []int accepts any length.

The length must be a constant, known at compile time:

const size = 4
var buf [size]byte      // fine

n := 4
var bad [n]byte         // compile error: array length n must be constant

len on an array is resolved at compile time, so it costs nothing at runtime.

Arrays are values

The difference from a slice that produces most surprises. Assignment copies the entire array:

a := [3]int{1, 2, 3}
b := a
b[0] = 99

fmt.Println(a)     // [1 2 3] — unchanged
fmt.Println(b)     // [99 2 3]

Compare a slice, where the header is copied and the backing array is shared:

s := []int{1, 2, 3}
t := s
t[0] = 99
fmt.Println(s)     // [99 2 3] — the same array

The same applies to function arguments:

func modify(arr [3]int)  { arr[0] = 99 }     // modifies a copy
func modifyP(arr *[3]int) { arr[0] = 99 }    // modifies the caller's array
func modifyS(s []int)    { s[0] = 99 }       // modifies the caller's data

So passing a [1000000]int by value copies eight megabytes on every call. Pass a pointer or, more idiomatically, a slice.

Go dereferences an array pointer for indexing and len, so arr[0] works on a *[3]int without writing (*arr)[0].

The range loop iterates a copy

A consequence of value semantics that is worth its own example:

a := [3]int{1, 2, 3}

for i, v := range a {
    a[2] = 99          // modifies the array
    fmt.Println(i, v)  // prints 1, 2, 3 — never 99
}
fmt.Println(a)         // [1 2 99]

range evaluates its expression once, and for an array that evaluation is a copy. The loop walks the snapshot, so writes to a during the loop are invisible to v.

On a slice, range copies the header, so the same code does see the change, the header points at the same backing array. Same syntax, different behaviour, decided by whether the operand is an array or a slice.

If the array is large, range over &a instead. Ranging over an array pointer does not copy:

for i, v := range &a {     // no copy of the array
    _ = i
    _ = v
}

Arrays are comparable

Unlike slices, arrays support == when the element type does:

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b)        // true — element by element

s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println(s1 == s2)      // compile error: slices can only be compared to nil

Which makes arrays usable as map keys, and slices not:

counts := map[[2]int]string{}
counts[[2]int{1, 2}] = "origin pair"      // fine

bad := map[[]int]string{}                  // compile error

That is a genuine use: a fixed-size coordinate, a small composite key, a checksum. When you have a slice and need a key, either copy it into an array of known length or format it into a string — knowing the string form allocates.

Where arrays are the right choice

Five cases, and outside them use a slice.

Fixed-size data whose size is part of its meaning. A hash is 32 bytes, an IPv4 address is 4, a UUID is 16. The type documents it and the compiler enforces it:

func Sum256(data []byte) [32]byte      // crypto/sha256 — the length is the contract

sha256.Sum256 returns an array precisely so callers cannot resize or alias the digest.

Avoiding an allocation. An array declared in a function can live on the stack; a slice usually escapes to the heap:

func encode(v uint64) string {
    var buf [20]byte                    // stack, no allocation
    return string(strconv.AppendUint(buf[:0], v, 10))
}

buf[:0] makes a zero-length slice over the array, so Append writes into the stack buffer. A common pattern in hot paths and in the standard library.

A comparable composite: as a map key, or so a containing struct stays comparable. One slice field makes a struct uncomparable; an array field does not.

Inside a struct where the data should be copied with it. A struct containing [4]byte copies that data on assignment; a struct containing []byte shares it. If the value is genuinely part of the struct, the array says so.

Multidimensional data of fixed shape. [8][8]Piece for a chessboard is contiguous memory with no per-row allocation, unlike [][]Piece.

Arrays and slices together

A slice can be taken over an array, sharing its storage:

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]                 // []int{2, 3, 4}, backed by a

s[0] = 99
fmt.Println(a)              // [1 99 3 4 5] — the same memory

fmt.Println(len(s), cap(s)) // 3 4 — capacity runs to the end of the array

a[:] is the idiomatic way to hand an array to something expecting a slice.

Going back the other way is newer. Go 1.17 added slice-to-array-pointer, and 1.20 added direct slice-to-array conversion:

s := []byte{1, 2, 3, 4}

p := (*[4]byte)(s)       // Go 1.17+: pointer, shares memory
arr := [4]byte(s)        // Go 1.20+: value, copies

var digest [32]byte
copy(digest[:], someSlice)     // works on every version

Both panic if the slice is shorter than the array. The conversion is genuinely useful when an API returns []byte and another wants [N]byte.

Frequently asked questions

What is the difference between an array and a slice?

An array has a fixed length that is part of its type and is copied on assignment. A slice is a header pointing at an array, with a length and capacity, and copying it shares the data.

Why can I not pass a [4]int where [3]int is expected?

The length is part of the type, so they are different types. Use a slice parameter to accept any length.

Why is my function not modifying the array?

It received a copy. Take a pointer (*[3]int) or use a slice.

Why does my range loop not see changes made inside it?

range evaluates the array once, which copies it. The loop walks the snapshot. Range over &a to avoid the copy, or use a slice, where the header points at shared data.

Can I use a variable for the array length?

No. It must be a compile-time constant. Use a slice with make for a runtime-determined length.

Can arrays be compared with ==?

Yes, element by element, if the element type is comparable. Slices cannot be compared to anything but nil.

Can an array be a map key?

Yes, when its element type is comparable. That is one of the main practical reasons to use one.

When should I prefer an array over a slice?

When the size is fixed and meaningful (a 32-byte hash), to avoid a heap allocation in a hot path, when you need comparability, or for fixed-shape multidimensional data.

How do I convert between them?

a[:] gives a slice over an array. [4]byte(s) copies a slice into an array (Go 1.20+), (*[4]byte)(s) takes a pointer to it (Go 1.17+). Both panic if the slice is too short.

Does len() on an array cost anything?

No. It is a compile-time constant.

Where should I go next?

Slices covers the type you will actually use most, and structs covers the value semantics arrays share.