Array #

An array in Go is a data structure whose size is determined at compile time and can’t change. In daily practice, Go developers more often use slices — but that doesn’t mean arrays are useless. In fact, understanding arrays properly is the key to understanding how slices work underneath, because a slice is essentially a “window” into an array. There are also specific cases where an array is the better choice than a slice: fixed-size buffers, fixed-size lookup tables, and matrices. This article covers arrays from the basics to their fundamental relationship with slices.

Conceptually, an array in Go is stored in memory as a contiguous block of fixed size. The array data structure in memory can be visualized in the following diagram:

flowchart TD
    subgraph ArrayMemory["[5]int Array in Memory"]
        direction LR
        Idx0["Index 0<br>Value: 0"]
        Idx1["Index 1<br>Value: 0"]
        Idx2["Index 2<br>Value: 0"]
        Idx3["Index 3<br>Value: 0"]
        Idx4["Index 4<br>Value: 0"]
    end
    
    style ArrayMemory fill:#f4f4f6,stroke:#333,stroke-width:2px

Arrays Are Value Types with Size as Part of the Type #

This is the most important thing to understand about arrays in Go: the array size is part of its type. [3]int and [5]int are two different types — just like int and string are different:

var a [3]int
var b [5]int

// a = b  // ← compile error: cannot use b (type [5]int) as type [3]int

// A function accepting [3]int can't accept [5]int
func sum3(arr [3]int) int { ... }
sum3(a)  // ✓
sum3(b)  // ✗ compile error

The practical implication: a function that receives an array must state its size explicitly, which makes it very inflexible. This is one of the main reasons slices are used more often — slices don’t have a size constraint in their type.


Declaration and Initialization #

Declaration with Zero Values #

var a [5]int      // [0 0 0 0 0] — all elements initialized to their zero value
var b [3]string   // ["" "" ""] — empty strings
var c [4]bool     // [false false false false]
var d [2]float64  // [0 0]

fmt.Println(a)  // [0 0 0 0 0]
fmt.Println(b)  // [  ] — three empty strings

Unlike C, arrays in Go never contain garbage values — every element is always initialized to its type’s zero value.

Initialization with Literals #

// All elements explicit
primes := [5]int{2, 3, 5, 7, 11}

// Partial — unspecified elements get their zero value
scores := [5]int{100, 95}       // [100 95 0 0 0]

// Initialization with specific indexes
sparse := [10]int{0: 1, 5: 10, 9: 100}
// [1 0 0 0 0 10 0 0 0 100]

// String array
days := [7]string{
    "Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday",
}

Ellipsis [...] — Size Automatically Derived from Contents #

Use [...] to let the compiler calculate the array size from the number of elements given:

// Size calculated automatically: 5 elements → [5]int
primes := [...]int{2, 3, 5, 7, 11}
fmt.Printf("Type: %T, Length: %d\n", primes, len(primes))
// Type: [5]int, Length: 5

// Handy so you don't have to count manually
colors := [...]string{
    "Red", "Orange", "Yellow",
    "Green", "Blue", "Indigo", "Violet",
}
// [7]string — 7 elements

Accessing and Modifying Elements #

Access elements using an index starting from 0:

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

// Read
fmt.Println(arr[0])   // 10
fmt.Println(arr[4])   // 50
fmt.Println(arr[len(arr)-1])  // 50 — the last element

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

Bounds Checking — Runtime Safety #

Go always checks array indexes at runtime. Accessing an index outside the range causes a panic:

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

fmt.Println(arr[2])  // ✓ 3 — valid index
fmt.Println(arr[3])  // ✗ panic: runtime error: index out of range [3] with length 3

// Negative indexes also panic
fmt.Println(arr[-1]) // ✗ compile error: invalid argument -1 (index must be non-negative)

Unlike C, Go doesn’t have buffer overflows. Accessing an array out of bounds causes a panic — the program stops with a clear error message, not dangerous undefined behavior. To avoid panics, always validate the index before accessing:

func safeGet(arr [5]int, i int) (int, bool) {
    if i < 0 || i >= len(arr) {
        return 0, false
    }
    return arr[i], true
}

Arrays Are Value Types — Copy Semantics #

Arrays in Go are value types — when you assign an array to another variable or pass it to a function, Go makes a complete copy of all elements:

a := [3]int{1, 2, 3}
b := a         // b is a COMPLETE COPY of a

b[0] = 999
fmt.Println(a)  // [1 2 3] — unchanged
fmt.Println(b)  // [999 2 3]

Implications for Functions #

Because arrays are passed by value, functions receive a copy — modifications inside the function don’t affect the original array:

// This function modifies a COPY, not the original array
func doubleAll(arr [5]int) [5]int {
    for i := range arr {
        arr[i] *= 2
    }
    return arr  // return the modified copy
}

func main() {
    original := [5]int{1, 2, 3, 4, 5}
    doubled := doubleAll(original)

    fmt.Println(original)  // [1 2 3 4 5] — unchanged
    fmt.Println(doubled)   // [2 4 6 8 10]
}

// If you want to modify the original, use a pointer
func doubleAllInPlace(arr *[5]int) {
    for i := range arr {
        arr[i] *= 2
    }
}

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    doubleAllInPlace(&arr)
    fmt.Println(arr)  // [2 4 6 8 10] — changed!
}

For large arrays, this copy semantics can become a performance bottleneck. The solutions: use a pointer to the array, or (more idiomatically) use a slice.


Comparing Arrays #

Arrays can be compared with == and != if the element type is comparable and the sizes match:

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

fmt.Println(a == b)  // true  — all elements equal
fmt.Println(a == c)  // false — last element differs
fmt.Println(a != c)  // true

// Different types can't be compared
d := [4]int{1, 2, 3, 4}
// fmt.Println(a == d)  // ← compile error: mismatched types [3]int and [4]int

// Arrays with non-comparable elements can't be compared
e := [2][]int{{1, 2}, {3, 4}}
f := [2][]int{{1, 2}, {3, 4}}
// fmt.Println(e == f)  // ← compile error: [2][]int is not comparable
_ = e
_ = f

This array comparability is useful for hash map keys — arrays can be used as map keys:

// Array as a map key — useful for coordinate mapping
type Point [2]int

grid := map[Point]string{
    {0, 0}: "origin",
    {1, 0}: "east",
    {0, 1}: "north",
}

fmt.Println(grid[Point{0, 0}])  // "origin"
fmt.Println(grid[Point{1, 0}])  // "east"

Iteration #

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

// Classic for
for i := 0; i < len(arr); i++ {
    fmt.Printf("arr[%d] = %d\n", i, arr[i])
}

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

// Value only
for _, v := range arr {
    fmt.Println(v)
}

// Index only
for i := range arr {
    arr[i] *= 2  // modify via the index (range gives a copy of the value)
}
fmt.Println(arr)  // [20 40 60 80 100]

// Reverse iteration
for i := len(arr) - 1; i >= 0; i-- {
    fmt.Println(arr[i])
}

Multidimensional Arrays #

Go supports arrays with more than one dimension. The most common is a two-dimensional array for matrices:

// 2D array — declaration
var matrix [3][4]int  // 3 rows, 4 columns — all zero values

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

// Access elements: [row][column]
fmt.Println(grid[0][0])  // 1 — row 0, column 0
fmt.Println(grid[1][2])  // 6 — row 1, column 2
fmt.Println(grid[2][2])  // 9 — row 2, column 2

// Modification
grid[1][1] = 99
fmt.Println(grid[1][1])  // 99

Matrix Traversal #

// Print a matrix in a neat format
func printMatrix(m [3][3]int) {
    for i, row := range m {
        for j, val := range row {
            fmt.Printf("%3d", val)
            if j < len(row)-1 {
                fmt.Print(" ")
            }
        }
        fmt.Println()
        _ = i
    }
}

// Transpose a matrix (swap rows and columns)
func transpose(m [3][3]int) [3][3]int {
    var result [3][3]int
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            result[j][i] = m[i][j]
        }
    }
    return result
}

// Matrix multiplication
func multiply(a, b [3][3]int) [3][3]int {
    var result [3][3]int
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            for k := 0; k < 3; k++ {
                result[i][j] += a[i][k] * b[k][j]
            }
        }
    }
    return result
}

3D Arrays #

// RGB image as a 3D array: [height][width][3]uint8
var image [480][640][3]uint8

// Set the pixel at (100, 200) to red
image[100][200][0] = 255  // R
image[100][200][1] = 0    // G
image[100][200][2] = 0    // B

The Relationship Between Arrays and Slices #

This is the most important concept to understand. A slice is a “window” into an array. Every slice has a backing array behind it. When you create a slice from an array, they share the same memory:

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

// Create a slice from the array — shares the backing array!
s := arr[1:4]  // the slice contains {20, 30, 40}

fmt.Println(arr)   // [10 20 30 40 50]
fmt.Println(s)     // [20 30 40]

// Modifying through the slice CHANGES the original array!
s[0] = 999
fmt.Println(arr)   // [10 999 30 40 50] — the array changed!
fmt.Println(s)     // [999 30 40]

// Modifying through the array also changes the slice
arr[2] = 777
fmt.Println(arr)   // [10 999 777 40 50]
fmt.Println(s)     // [999 777 40] — the slice changed!

Understanding this relationship is the key to understanding slice behavior, which is covered in depth in the next article.


When to Use an Array vs a Slice #

This is a question that comes up often. The practical guide:

USE AN ARRAY if:
  ✓ The size is truly fixed and known at compile time
  ✓ The size is small and copy semantics isn't a performance concern
  ✓ You need an array as a map key (slices can't be keys)
  ✓ Implementing matrix algorithms with a fixed size
  ✓ Fixed-size buffers at a low level (byte buffers for protocols)
  ✓ Alias types like [16]byte for UUIDs or [32]byte for hashes

USE A SLICE for all other cases:
  ✓ The size isn't known at compile time
  ✓ You need to add or remove elements
  ✓ Functions that must work with collections of various sizes
  ✓ Almost all everyday collection operations

Rule of thumb: start with a slice. Switch to an array only
if there's a specific technical reason.

Real Array Use Cases #

Fixed-Size Identifiers #

// UUID — always 16 bytes
type UUID [16]byte

func NewUUID() UUID {
    var id UUID
    rand.Read(id[:])  // fill with random bytes
    return id
}

// SHA-256 hash — always 32 bytes
type Hash [32]byte

func hashData(data []byte) Hash {
    return sha256.Sum256(data)  // returns a [32]byte
}

Lookup Tables #

// Month names — fixed size of 12
var months = [12]string{
    "January", "February", "March", "April",
    "May", "June", "July", "August",
    "September", "October", "November", "December",
}

func monthName(m int) string {
    if m < 1 || m > 12 {
        return "Invalid"
    }
    return months[m-1]
}

// Days per month (non-leap year)
var daysInMonth = [12]int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

Fixed-Size Buffers for Network Protocols #

// HTTP/2 frame header — always 9 bytes
type FrameHeader [9]byte

func (h FrameHeader) Length() int {
    return int(h[0])<<16 | int(h[1])<<8 | int(h[2])
}

func (h FrameHeader) Type() byte {
    return h[3]
}

func (h FrameHeader) Flags() byte {
    return h[4]
}

Complete Example Program #

The following program implements various matrix operations using multidimensional arrays:

package main

import (
    "fmt"
    "math"
)

const N = 3  // matrix size

type Matrix [N][N]float64

// Create an identity matrix
func identity() Matrix {
    var m Matrix
    for i := 0; i < N; i++ {
        m[i][i] = 1  // diagonal = 1, everything else 0 (zero value)
    }
    return m
}

// Add two matrices
func add(a, b Matrix) Matrix {
    var result Matrix
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            result[i][j] = a[i][j] + b[i][j]
        }
    }
    return result
}

// Multiply two matrices
func multiply(a, b Matrix) Matrix {
    var result Matrix
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            for k := 0; k < N; k++ {
                result[i][j] += a[i][k] * b[k][j]
            }
        }
    }
    return result
}

// Transpose — swap rows and columns
func transpose(m Matrix) Matrix {
    var result Matrix
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            result[j][i] = m[i][j]
        }
    }
    return result
}

// Calculate the trace — the sum of the main diagonal
func trace(m Matrix) float64 {
    sum := 0.0
    for i := 0; i < N; i++ {
        sum += m[i][i]
    }
    return sum
}

// Frobenius norm — the "size" of a matrix
func frobeniusNorm(m Matrix) float64 {
    sum := 0.0
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            sum += m[i][j] * m[i][j]
        }
    }
    return math.Sqrt(sum)
}

// Multiply a matrix by a scalar
func scale(m Matrix, s float64) Matrix {
    var result Matrix
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            result[i][j] = m[i][j] * s
        }
    }
    return result
}

// Print a matrix in a neat format
func print(label string, m Matrix) {
    fmt.Printf("%s:\n", label)
    for _, row := range m {
        fmt.Print("  [")
        for j, val := range row {
            if j > 0 {
                fmt.Print(", ")
            }
            fmt.Printf("%6.1f", val)
        }
        fmt.Println("]")
    }
}

// Compare two matrices (with a floating-point tolerance)
func equal(a, b Matrix, epsilon float64) bool {
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            if math.Abs(a[i][j]-b[i][j]) > epsilon {
                return false
            }
        }
    }
    return true
}

func main() {
    // Matrices A and B
    A := Matrix{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }

    B := Matrix{
        {9, 8, 7},
        {6, 5, 4},
        {3, 2, 1},
    }

    I := identity()

    fmt.Println("=== Matrix Operations ===\n")

    print("A", A)
    fmt.Println()
    print("B", B)
    fmt.Println()
    print("I (Identity)", I)
    fmt.Println()

    // Basic operations
    print("A + B", add(A, B))
    fmt.Println()

    print("A × B", multiply(A, B))
    fmt.Println()

    print("Transpose(A)", transpose(A))
    fmt.Println()

    print("2 × A", scale(A, 2))
    fmt.Println()

    // Properties
    fmt.Printf("Trace(A)            = %.1f\n", trace(A))
    fmt.Printf("Frobenius Norm(A)   = %.4f\n", frobeniusNorm(A))
    fmt.Println()

    // Verify matrix properties
    // A × I = A
    AI := multiply(A, I)
    fmt.Printf("A × I == A          = %v\n", equal(AI, A, 1e-9))

    // (A^T)^T = A
    ATT := transpose(transpose(A))
    fmt.Printf("Transpose(Transpose(A)) == A = %v\n", equal(ATT, A, 1e-9))

    // Trace(A^T) = Trace(A)
    fmt.Printf("Trace(Transpose(A)) = Trace(A) = %v\n",
        math.Abs(trace(transpose(A))-trace(A)) < 1e-9)

    fmt.Println()

    // Demonstrate: array as a map key
    type Coord [2]int
    locationNames := map[Coord]string{
        {0, 0}: "Origin",
        {1, 0}: "East",
        {0, 1}: "North",
        {-1, 0}: "West",
        {0, -1}: "South",
    }

    fmt.Println("=== Coordinates as Map Keys ===")
    position := Coord{1, 0}
    if name, ok := locationNames[position]; ok {
        fmt.Printf("Position %v = %s\n", position, name)
    }

    // Demonstrate: copy semantics
    fmt.Println("\n=== Copy Semantics ===")
    original := [3]int{1, 2, 3}
    copyArr := original        // complete copy
    copyArr[0] = 999
    fmt.Printf("Original: %v\n", original)  // [1 2 3] — unchanged
    fmt.Printf("Copy:     %v\n", copyArr)   // [999 2 3]

    // Demonstrate: a slice from an array shares memory
    fmt.Println("\n=== Array as Slice Backing Storage ===")
    arr := [5]int{10, 20, 30, 40, 50}
    sl := arr[1:4]
    fmt.Printf("Initial array: %v\n", arr)
    fmt.Printf("Slice [1:4]: %v\n", sl)
    sl[0] = 999
    fmt.Printf("Array after sl[0]=999: %v\n", arr)  // arr changed!
    fmt.Printf("Slice after sl[0]=999: %v\n", sl)
}

Summary #

  • The array size is part of its type[3]int and [5]int are different types; they can’t be passed to the same function.
  • Zero values are guaranteed — every array element is always initialized to its type’s zero value; no garbage values like in C.
  • [...] lets the compiler calculate the size from the elements given during initialization.
  • Arrays are value types — assignment and passing to functions make a complete copy of all elements.
  • Runtime bounds checking — accessing an index out of range causes a panic with a clear message.
  • Arrays are comparable (if their elements are) — usable as map keys; useful for coordinates and fixed identifiers.
  • Multidimensional arrays[row][column]T; traversal with nested loops; useful for matrices and images.
  • Arrays are slice backing storage — a slice created from an array shares memory with the original array.
  • Use arrays for fixed sizes known at compile time, map keys, fixed-size identifiers (UUID, hashes), and matrix algorithms.
  • Use slices for almost all other collection needs — more flexible and more idiomatic in Go.

← Previous: Exceptions   Next: Slice →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact