Go Arrays: Fixed-Size Collections
Arrays in Go are fixed-size, homogeneous collections where every element must be of the same type. Unlike slices, which are the more commonly used collection type in Go, arrays have their size baked...
Key Insights
- Arrays in Go are value types with fixed sizes that become part of their type signature—
[3]intand[4]intare completely different types that cannot be interchanged. - Assigning an array or passing it to a function creates a full copy of all elements, making arrays inefficient for large datasets but predictable for small, fixed collections.
- Use arrays for truly fixed-size data like RGB values or coordinate pairs; use slices for everything else—slices are the idiomatic choice in 99% of Go code.
Introduction to Arrays in Go
Arrays in Go are fixed-size, homogeneous collections where every element must be of the same type. Unlike slices, which are the more commonly used collection type in Go, arrays have their size baked into their type definition. An array of 5 integers ([5]int) is a fundamentally different type from an array of 6 integers ([6]int), and you cannot assign one to the other.
The most critical characteristic of Go arrays is that they are value types, not reference types. When you assign an array to another variable or pass it to a function, Go copies the entire array. This behavior contrasts sharply with arrays in languages like JavaScript, Python, or Java, where arrays are reference types.
You should reach for arrays when you have a genuinely fixed-size collection that won’t grow or shrink: RGB color values, 3D coordinates, fixed-size buffers, or lookup tables with a known number of elements. For everything else—and that’s most use cases—use slices.
package main
import "fmt"
func main() {
// Array: fixed size, part of the type
var arr [3]int
arr[0] = 10
arr[1] = 20
arr[2] = 30
fmt.Println(arr) // [10 20 30]
fmt.Printf("Type: %T\n", arr) // Type: [3]int
}
Declaring and Initializing Arrays
Go offers several ways to declare and initialize arrays, each suited to different situations. The most verbose approach uses var with explicit type declaration, which initializes all elements to their zero values.
// Zero-value initialization
var numbers [5]int // [0 0 0 0 0]
var names [3]string // ["" "" ""]
var flags [4]bool // [false false false false]
// Literal initialization with explicit size
colors := [3]string{"red", "green", "blue"}
// Compiler-inferred length using ...
primes := [...]int{2, 3, 5, 7, 11, 13}
fmt.Println(len(primes)) // 6
// Partial initialization - unspecified elements get zero values
partial := [5]int{1, 2, 3} // [1 2 3 0 0]
// Indexed initialization
sparse := [10]int{2: 100, 5: 200} // [0 0 100 0 0 200 0 0 0 0]
The ... syntax is particularly useful when you want the compiler to count the elements for you. This approach reduces maintenance burden—if you add or remove elements, you don’t need to update the size manually. However, remember that the resulting type is still a fixed-size array based on the number of elements you provided.
Indexed initialization lets you set specific positions while leaving others at zero values, which is handy for sparse arrays or when you want to emphasize particular indices.
Accessing and Modifying Array Elements
Array access uses zero-based indexing with square brackets. Go performs bounds checking at runtime, so attempting to access an index outside the array’s bounds causes a panic.
package main
import "fmt"
func main() {
scores := [5]int{100, 85, 90, 78, 92}
// Index-based access
fmt.Println(scores[0]) // 100
fmt.Println(scores[4]) // 92
// Modification
scores[1] = 88
fmt.Println(scores) // [100 88 90 78 92]
// Length
fmt.Println(len(scores)) // 5
// Traditional for loop
for i := 0; i < len(scores); i++ {
fmt.Printf("Score %d: %d\n", i, scores[i])
}
// Range-based iteration (idiomatic)
for index, value := range scores {
fmt.Printf("scores[%d] = %d\n", index, value)
}
// Range with value only (index ignored)
sum := 0
for _, value := range scores {
sum += value
}
fmt.Println("Average:", sum/len(scores))
// This would panic at runtime:
// fmt.Println(scores[10]) // panic: runtime error: index out of range
}
The range keyword provides the idiomatic way to iterate over arrays. It returns both the index and value on each iteration. Use the blank identifier _ to ignore the index when you only need values.
Arrays as Value Types
This is where arrays diverge sharply from your intuition if you come from other languages. Arrays are value types, meaning assignment and function calls create complete copies.
package main
import "fmt"
func modifyArray(arr [3]int) {
arr[0] = 999
fmt.Println("Inside function:", arr) // [999 2 3]
}
func modifyArrayPointer(arr *[3]int) {
arr[0] = 999
fmt.Println("Inside function:", *arr) // [999 2 3]
}
func main() {
original := [3]int{1, 2, 3}
// Assignment creates a copy
copied := original
copied[0] = 100
fmt.Println("Original:", original) // [1 2 3]
fmt.Println("Copied:", copied) // [100 2 3]
// Function call copies the array
modifyArray(original)
fmt.Println("After modifyArray:", original) // [1 2 3] - unchanged!
// Pass a pointer to modify the original
modifyArrayPointer(&original)
fmt.Println("After modifyArrayPointer:", original) // [999 2 3]
}
For small arrays (a few elements), copying is cheap and the value semantics provide clear, predictable behavior. For larger arrays, the copying overhead becomes significant. If you need to pass large arrays to functions and modify them, pass a pointer. Better yet, use a slice instead—slices are designed for this use case.
Multidimensional Arrays
Go supports multidimensional arrays by nesting array types. The most common use case is 2D arrays for matrices, grids, or tables.
package main
import "fmt"
func main() {
// 2D array: 3 rows, 4 columns
var matrix [3][4]int
// Initialize with values
grid := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// Access elements
fmt.Println(grid[0][1]) // 2
fmt.Println(grid[1][2]) // 6
// Modify elements
grid[1][1] = 50
// Iterate with nested loops
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[i]); j++ {
fmt.Printf("%3d ", grid[i][j])
}
fmt.Println()
}
// Range-based iteration
for i, row := range grid {
for j, value := range row {
fmt.Printf("grid[%d][%d] = %d\n", i, j, value)
}
}
// Simple matrix addition
a := [2][2]int{{1, 2}, {3, 4}}
b := [2][2]int{{5, 6}, {7, 8}}
var result [2][2]int
for i := range a {
for j := range a[i] {
result[i][j] = a[i][j] + b[i][j]
}
}
fmt.Println("Sum:", result) // [[6 8] [10 12]]
}
Remember that multidimensional arrays are also value types, so a [100][100]int array passed to a function copies all 10,000 integers. For non-trivial matrix operations, use slices of slices or specialized libraries.
Arrays vs Slices: When to Use Each
Slices are the dominant collection type in Go for good reasons: they’re flexible, efficient, and have convenient built-in functions like append. Arrays have their place, but it’s a narrow one.
Use arrays when:
- The size is truly fixed and known at compile time
- You want value semantics and immutability guarantees
- You’re working with small collections (< 100 elements)
- You need stack allocation for performance-critical code
Use slices when:
- The size might change
- You’re passing collections to functions
- You’re working with large datasets
- You need built-in growth capabilities
package main
import "fmt"
func main() {
// Array to slice conversion
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:] // Creates a slice backed by the array
fmt.Printf("Array: %v, type: %T\n", arr, arr)
fmt.Printf("Slice: %v, type: %T\n", slice, slice)
// Modifying the slice affects the underlying array
slice[0] = 999
fmt.Println("Array after slice modification:", arr) // [999 2 3 4 5]
// Slice from array range
middle := arr[1:4]
fmt.Println("Middle elements:", middle) // [2 3 4]
// Creating a slice from an array literal
quickSlice := []int{1, 2, 3} // This is a slice, not an array!
fmt.Printf("Type: %T\n", quickSlice) // []int
}
Notice the subtle syntax difference: [3]int{1,2,3} is an array, while []int{1,2,3} is a slice. The missing size makes it a slice.
Practical Use Cases and Best Practices
Arrays shine in specific scenarios where their constraints become advantages. Here are real-world examples where arrays are the right choice.
package main
import "fmt"
// RGB color representation
type Color [3]uint8
func (c Color) String() string {
return fmt.Sprintf("RGB(%d, %d, %d)", c[0], c[1], c[2])
}
// 3D point in space
type Point3D [3]float64
func (p Point3D) Magnitude() float64 {
return p[0]*p[0] + p[1]*p[1] + p[2]*p[2]
}
// Fixed-size circular buffer
type CircularBuffer struct {
data [8]int
head int
count int
}
func (cb *CircularBuffer) Push(value int) {
cb.data[cb.head] = value
cb.head = (cb.head + 1) % len(cb.data)
if cb.count < len(cb.data) {
cb.count++
}
}
// Lookup table for day names
var dayNames = [7]string{
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday",
}
func getDayName(dayIndex int) string {
if dayIndex >= 0 && dayIndex < len(dayNames) {
return dayNames[dayIndex]
}
return "Invalid day"
}
func main() {
red := Color{255, 0, 0}
fmt.Println(red) // RGB(255, 0, 0)
origin := Point3D{0, 0, 0}
fmt.Println("Magnitude:", origin.Magnitude())
buffer := &CircularBuffer{}
for i := 1; i <= 10; i++ {
buffer.Push(i)
}
fmt.Println("Buffer:", buffer.data) // Last 8 values
fmt.Println(getDayName(3)) // Wednesday
}
These examples demonstrate arrays’ sweet spot: small, fixed-size data structures where the size is inherent to the concept. An RGB color always has exactly three components. A day-of-week lookup table always has seven entries. A 3D point always has three coordinates.
For performance-critical code, arrays can be allocated on the stack rather than the heap, reducing garbage collection pressure. However, this optimization only matters for hot paths in performance-sensitive applications. Don’t prematurely optimize by using arrays everywhere—the ergonomic benefits of slices usually outweigh the minor performance gains.
The bottom line: arrays are a specialized tool in Go. Understand them because they’re foundational to slices, and use them when their constraints match your problem domain. For most Go code, slices are the better choice.