Skip to main content

Go Arrays: Fixed-Size Collections

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 4 min read

Arrays in Go are fixed-size, value-type sequences. The size is part of the type — [3]int and [5]int are different types and cannot be assigned to each other. In practice, you’ll work with slices (built on top of arrays) far more often, but understanding arrays explains slice behavior — particularly why modifying a slice can affect other slices that share the same underlying array.

For slices see Go slices dynamic arrays.

Declaring Arrays

// Zero-initialized by default
var scores [5]int  // [0 0 0 0 0]

// Array literal
colors := [3]string{"red", "green", "blue"}

// Ellipsis: compiler counts the elements for you
primes := [...]int{2, 3, 5, 7, 11}  // [5]int
fmt.Println(len(primes))              // 5

// Partial initialization — remaining elements are zero-valued
partial := [5]int{1, 2, 3}  // [1 2 3 0 0]

// Specific index initialization
sparse := [5]int{0: 10, 4: 50}  // [10 0 0 0 50]

The size must be a constant — you can’t use a variable as the array size. When the size is unknown at compile time, use a slice.

Indexing and Modification

Arrays are zero-indexed. Accessing out-of-bounds indices is a compile-time error for constant indices, and a runtime panic for variable indices:

arr := [5]int{10, 20, 30, 40, 50}

fmt.Println(arr[0])  // 10
fmt.Println(arr[4])  // 50
// fmt.Println(arr[5])  // compile error: index out of range

arr[2] = 99
fmt.Println(arr)  // [10 20 99 40 50]

Value Semantics: Arrays Are Copied

Unlike slices, arrays are value types — assigning or passing an array copies all its elements. Modifying the copy doesn’t affect the original:

a := [3]int{1, 2, 3}
b := a        // b is a full copy
b[0] = 99
fmt.Println(a)  // [1 2 3] — unchanged
fmt.Println(b)  // [99 2 3]

// Same when passed to a function
func double(arr [3]int) {
    arr[0] *= 2  // modifies the copy, not the original
}
double(a)
fmt.Println(a)  // [1 2 3] — still unchanged

To allow a function to modify the original, pass a pointer to the array — or better, pass a slice derived from it:

func doubleAll(arr *[3]int) {
    for i := range arr {
        arr[i] *= 2
    }
}
doubleAll(&a)
fmt.Println(a)  // [2 4 6]

Slicing Arrays

A slice expression on an array creates a slice that shares the array’s underlying storage:

arr := [5]int{1, 2, 3, 4, 5}

s := arr[1:4]   // [2 3 4] — shares arr's memory
s[0] = 20
fmt.Println(arr)  // [1 20 3 4 5] — arr is modified through s!
fmt.Println(s)    // [20 3 4]

The three-index form limits the slice capacity, preventing appends from overwriting elements beyond the slice:

// arr[low:high:max] — cap = max-low
s2 := arr[1:3:3]  // len=2, cap=2
s2 = append(s2, 99)  // forces a new allocation — arr is unaffected

Iteration

arr := [5]int{10, 20, 30, 40, 50}

// range is idiomatic — index and value
for i, v := range arr {
    fmt.Printf("arr[%d] = %d\n", i, v)
}

// Value only (index discarded)
sum := 0
for _, v := range arr {
    sum += v
}

// Reverse iteration
for i := len(arr) - 1; i >= 0; i-- {
    fmt.Print(arr[i], " ")  // 50 40 30 20 10
}

Multidimensional Arrays

Go supports arrays of arrays. The inner arrays are stored contiguously for two-dimensional cases:

// 3×3 matrix
matrix := [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

fmt.Println(matrix[1][2])  // 6 — row 1, column 2

// Iterate row-major (cache-friendly)
for i := range matrix {
    for j := range matrix[i] {
        fmt.Printf("%d ", matrix[i][j])
    }
    fmt.Println()
}

For large or variable-sized matrices, use a slice of slices ([][]int) instead of an array of arrays.

Arrays vs Slices: When to Use Each

Use arrays when:

  • The size is fixed and known at compile time (color channels: [3]uint8, coordinates: [2]float64, SHA256 hash: [32]byte)
  • You want value semantics — copying an array is correct behavior
  • You need comparability — arrays are comparable with ==, slices are not

Use slices when:

  • The size varies or is unknown
  • You need to pass to most standard library functions (which expect slices)
  • You want reference semantics — multiple things should see the same data
// Fixed-size use cases — arrays make sense
var pixel [3]byte      // RGB values
var hash [32]byte      // SHA-256 result
var ipv4 [4]byte       // IPv4 address

// Variable-size — use slices
var names []string
var ids []int64

The standard library uses fixed-size arrays for things with genuinely fixed dimensions. sha256.Sum256 returns [32]byte, not a slice — converting it to a slice when needed is explicit: hash := sha256.Sum256(data); hex.EncodeToString(hash[:]).

Summary

  • Array size is part of the type — [3]int[5]int; use [...]int{...} to let the compiler count
  • Arrays are value types — assignment and function passing copies all elements
  • Slicing an array creates a slice sharing the same memory — modifications propagate
  • Use three-index slicing arr[l:h:m] to prevent appends from overwriting beyond the slice boundary
  • Prefer slices for most use cases; reach for arrays when size is fixed and value semantics are desired

Resources

Comments

👍 Was this article helpful?