Go Programming Series: This lesson is part of our comprehensive Complete Go Tutorial: Learn Go (Golang) from Beginner to Advanced.
Go Arrays and Multidimensional Arrays: Declaration, Operations and Best Practices
Arrays are fixed-size sequences of values of the same type. In Go, an array is more than a container: its length is part of its type, which affects assignment, function parameters, comparison, and API design. Arrays are useful when the number of elements is known and fixed, when a value should have a predictable shape, or when you deliberately want value semantics. This guide explains arrays from first principles through multidimensional arrays, copying, comparison, function design, memory behavior, and the point at which a slice is usually a better choice.
The goal is not to treat arrays as a beginner-only feature. A developer who understands the difference between an array and a slice can make better decisions about ownership, copying, allocation, and API boundaries. Those decisions become increasingly important in production code, data processing, protocol handling, and performance-sensitive paths.
What You Will Learn
- What a Go array is and why its length belongs to its type
- How to declare, initialize, index, iterate over, and copy arrays
- How zero values work with arrays
- How multidimensional arrays are represented
- How arrays behave when passed to functions or returned from functions
- How arrays differ from slices in semantics and practical use
- When arrays are comparable and useful as map keys
- Common bounds, initialization, and API-design mistakes
- How to choose arrays deliberately in production code
1. What Is an Array in Go?
An array has a fixed length and an element type. The declaration [5]int describes five integers, while [10]int describes ten integers. These are different types. This is one of the most important properties of Go arrays: the compiler knows the size as part of the type.
var scores [5]int
scores[0] = 91
scores[1] = 87
names := [3]string{"Asha", "Ravi", "Mina"}
ids := [...]int{10, 20, 30, 40}
The […] form asks the compiler to determine the array length from the initializer. It is still an array with a fixed length after compilation; it does not become a slice.
2. Declaration and Initialization
The zero value of an array is an array whose elements are the zero value of the element type. An [5]int therefore starts with five zeros. This makes arrays convenient when a complete fixed-size value can be built incrementally without a separate initialization step.
var counts [4]int
fmt.Println(counts) // [0 0 0 0]
letters := [5]byte{'a', 'b', 'c', 'd', 'e'}
partial := [5]int{1, 2} // remaining elements are zero
Indexed composite literals can initialize selected positions. This is useful when the position itself has meaning and makes large fixed-size declarations easier to read.
days := [7]string{
0: "Sunday",
1: "Monday",
6: "Saturday",
}
3. Indexing, Length and Bounds
Array indexes start at zero. For an array of length n, valid indexes are 0 through n-1. The built-in len function returns the number of elements. An invalid index is a runtime panic when the index is not known to be invalid at compile time.
values := [4]int{10, 20, 30, 40}
fmt.Println(values[0])
fmt.Println(values[len(values)-1])
Use len rather than repeating a literal size in loops. This prevents a maintenance problem in which the array length changes but a loop still assumes the old value.
for i := 0; i < len(values); i++ {
fmt.Println(i, values[i])
}
4. Iterating Over Arrays
The range form is usually the clearest way to process every element. It provides an index and a copy of the element. If the element is large, remember that the range variable is a value copy; use the index when you intentionally need to modify the array or avoid copying a large value.
for i, value := range values {
fmt.Println(i, value)
}
for i := range values {
values[i] *= 2
}
When the index is not needed, use the blank identifier. Keeping iteration idiomatic improves readability and reduces opportunities for off-by-one errors.
5. Arrays and the Zero Value
Go's zero-value design means an array can often be declared without a constructor or initialization function. For numeric types the zero is 0, for bool it is false, for strings it is the empty string, and for pointer-like elements it is nil. This property is especially useful for fixed-size buffers and state tables.
Zero values do not mean that every array is automatically valid for every application. A zero entry may be a legitimate value or may mean 'not initialized' depending on the domain. If the distinction matters, model it explicitly rather than relying on an undocumented sentinel.
6. Arrays Are Values
Assigning an array copies the entire array. The two variables then contain independent array values. Updating one does not mutate the other.
a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [99 2 3]
This value semantics is different from the common mental model people bring from slices. It can be desirable when a fixed-size value should be passed around without sharing mutable storage. It can also be expensive if the array is very large, so API design should consider whether copying is appropriate.
7. Arrays as Function Parameters and Return Values
An array parameter is passed by value, so the function receives a copy of the array. This is useful when you intentionally want value semantics, but it can be surprising if you expect in-place mutation.
func total(values [4]int) int {
sum := 0
for _, v := range values {
sum += v
}
return sum
}
If the function should modify the caller's array, pass a pointer to the array or use a slice when fixed length is not part of the contract.
func normalize(values *[4]int) {
for i := range values {
values[i] = values[i] / 10
}
}
8. Multidimensional Arrays
A multidimensional array is an array whose element type is another array. A [3][3]int can represent a three-by-three grid. This is a natural representation for fixed-size matrices, boards, lookup tables, and other rectangular data.
var board [3][3]string
board[1][2] = "X"
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
The dimensions are part of the type. A [2][3]int is not interchangeable with a [3][2]int even though both contain six integers. This strong typing can prevent accidental shape mismatches.
for row := range matrix {
for col := range matrix[row] {
fmt.Printf("%d ", matrix[row][col])
}
fmt.Println()
}
9. Comparing Arrays
Arrays are comparable when their element type is comparable. Two arrays of the same type can therefore be compared with == and !=. Comparison proceeds element by element. This can be useful for fixed-size identifiers or small state values.
a := [3]byte{1, 2, 3}
b := [3]byte{1, 2, 3}
fmt.Println(a == b) // true
If an array contains a non-comparable element type, the array is not comparable. For example, an array whose elements are slices cannot be compared with == because slices themselves are not comparable.
10. Arrays vs Slices
| Property | Array | Slice |
|---|---|---|
| Size | Fixed and part of type | Dynamic length view over an array |
| Assignment | Copies all elements | Copies slice header; underlying storage may be shared |
| Resizing | Not supported | append can grow the slice |
| Typical use | Fixed-size data | Most variable-size collections |
| Comparable | Yes when element type is comparable | No |
| Function APIs | Can enforce exact length | Usually more flexible |
For most application-level collections, slices are the idiomatic choice. Arrays become attractive when the exact size is meaningful, when you want value semantics, or when an API needs a fixed-size type such as a cryptographic key or protocol field.
11. Practical Use Cases
- Fixed-size buffers where capacity is deliberately bounded
- Small cryptographic or protocol identifiers represented as byte arrays
- Matrices and fixed game-board layouts
- Lookup tables whose size is part of the algorithm
- Compile-time-sized records where exact shape matters
- Testing values where independent copying is desirable
12. Performance and Memory Considerations
An array's storage is contiguous as part of the value, which gives predictable layout and good cache behavior. However, copying a large array copies every element. The right question is not whether arrays are 'faster' than slices in every case, but whether their fixed-size value semantics match the workload.
When an array is embedded in a larger struct, the array contributes directly to that struct's size. Passing large structs containing arrays by value can increase copying costs. In performance-sensitive code, measure with benchmarks rather than assuming a representation is faster.
13. Common Mistakes and Troubleshooting
- Using an invalid index and triggering an out-of-range panic
- Assuming [N]T and []T are interchangeable
- Forgetting that array assignment copies all elements
- Passing a large array by value without considering copy cost
- Hard-coding a loop limit instead of using len
- Assuming multidimensional arrays are slices of slices
- Using arrays where the collection size is naturally variable
14. Best Practices
Use arrays when the fixed length is part of the meaning of the data.
Prefer slices for general-purpose variable-length collections.
Use len in loops rather than duplicated size constants.
Use range for straightforward iteration.
Be deliberate about array copying at API boundaries.
Use multidimensional arrays when the dimensions are fixed and meaningful.
Benchmark large-value operations before optimizing representation.
15. Testing Array-Based Code
Tests should cover the zero value, first and last indexes, expected full length, boundary behavior, and multidimensional positions. If a function accepts a fixed-size array, include tests that demonstrate the exact contract rather than converting everything to slices just for convenience.
func TestFirstValue(t *testing.T) {
values := [3]int{10, 20, 30}
if values[0] != 10 {
t.Fatal("unexpected first value")
}
}
Frequently Asked Questions
Are arrays dynamic in Go?
No. Their length is fixed and forms part of the array type. Use a slice when the number of elements can change.
What is the difference between [3]int and []int?
[3]int is a three-element array with a fixed size. []int is a slice, a flexible descriptor over an underlying array.
Does assigning an array copy it?
Yes. Arrays have value semantics, so assignment copies the elements.
Can arrays be compared?
Yes, if the element type is comparable and both arrays have the same array type.
Should I use arrays instead of slices for performance?
Not automatically. Arrays can have useful layout and value semantics, but copying large arrays can be costly. Benchmark the actual workload.
Can a multidimensional array have different row lengths?
No. Each nested array has the same fixed type and therefore the same length. If rows have different lengths, a slice-based representation is more appropriate.
Article Takeaways
- An array's length is part of its type.
- Arrays use value semantics and are copied on assignment.
- Use len and range for clear iteration.
- Multidimensional arrays are arrays of arrays with fixed dimensions.
- Slices are generally preferable for variable-size collections.
- Choose arrays when fixed size and value semantics are meaningful.
Conclusion
Go arrays are simple, strongly typed, and intentionally predictable. Their fixed length can be an advantage rather than a limitation when the shape of the data is part of the problem. Understanding array value semantics, copying, multidimensional layouts, and the difference from slices gives you a stronger foundation for designing Go APIs and data structures. Use arrays deliberately, use slices when flexibility is required, and let the data model—not habit—drive the choice.