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:2pxArrays 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]intand[5]intare 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.