Array Declaration and Type in Go
How Go array types work, why the length is part of the type, zero-value behavior, and the practical implications of fixed-size value-type collections.
An array in Go is a numbered sequence of elements that all share the same type. What trips up newcomers is that the size is not just a property — it is part of the type itself. A [4]int and a [5]int are two completely different types, as different from each other as int and string. This page explains how to declare an array, what that type signature means, and why Go designed it that way.
The Array Type Signature
Every array type follows the pattern [n]T. n is the number of elements, and T is the type of each element. n must be a compile‑time constant — you cannot use a variable to set the length.
var scores [5]int // An array that holds exactly 5 integers
var names [3]string // An array that holds exactly 3 strings
var flags [10]bool // An array that holds exactly 10 booleans
The type is [5]int, not []int. The length lives inside the brackets and changes the type. This has two immediate consequences:
- You cannot assign a
[3]intto a variable typed[5]int— the compiler will stop you. - You cannot resize an array at runtime. The length is locked in at compile time.
Compile‑time constant:
The size n must be known at compile time. A variable like size := 5; var a [size]int will not compile. You either hardcode a literal or use a constant.
Declaring an Array with var
The most explicit way to declare an array is with the var keyword. When you declare an array this way, every element is automatically set to the zero value of the element type.
package main
import "fmt"
func main() {
var a [4]int
var b [2]string
var c [3]bool
fmt.Println("ints:", a) // Output: [0 0 0 0]
fmt.Println("strings:", b) // Output: [ ]
fmt.Println("bools:", c) // Output: [false false false]
}
Zero values are predictable and consistent: 0 for numbers, "" for strings, false for booleans. There is no “uninitialized” state — the entire array is usable immediately.
This is a safety feature. In languages where arrays can contain garbage values, bugs creep in when you read before writing. Go eliminates that class of bug entirely.
Shorthand Declaration Without Explicit Initial Values
Inside a function, you can use := with an empty composite literal to declare an array that starts with all zero values. The syntax is variable := [size]Type{}.
func main() {
a := [3]float64{}
fmt.Println(a) // Output: [0 0 0]
}
The result is identical to var a [3]float64. The empty braces are necessary to trigger the composite literal syntax, but since no values are listed, every element defaults to zero.
Consistent initialization:
Whether you use var or := []Type{}, you get a fully zeroed array. This means you never have to manually fill defaults — a loop over the array is safe right after declaration.
Why the Length Is Part of the Type
If you come from languages like Python, JavaScript, or Java, this is the single most important thing to internalize. In Go, [3]int and [5]int are not just arrays of different sizes; they are different types. The compiler enforces this strictly.
func main() {
a := [3]int{1, 2, 3}
var b [5]int
b = a // COMPILE ERROR: cannot use a (type [3]int) as type [5]int
}
This design is not arbitrary. It gives the compiler precise knowledge about the memory footprint of every array variable. A [3]int is exactly 24 bytes on a 64‑bit system (3 × 8 bytes). A [5]int is 40 bytes. The type system guarantees that a function expecting a [3]int will never accidentally receive a differently‑sized block of memory.
From a beginner’s mental model: think of the length as being part of the type name itself, like a serial number stamped into the metal. You don’t say “this is an array of ints that happens to be size 3.” You say “this is a 3‑int‑array,” and a 5‑int‑array is a completely different object.
Assignment across sizes fails at compile time:
Trying to assign a [n]T to a [m]T when n ≠ m is a compile‑time error, not a runtime panic. That means you catch the mistake before your code ever runs.
Arrays Are Value Types
Arrays in Go are value types, not reference types. When you assign an array to another variable, the entire array is copied. The new variable owns an independent block of memory. Changes to the copy never affect the original.
func main() {
original := [3]string{"Go", "Python", "Rust"}
clone := original
clone[0] = "JavaScript"
fmt.Println("original:", original) // [Go Python Rust]
fmt.Println("clone:", clone) // [JavaScript Python Rust]
}
This behavior matters most when arrays are passed to functions. The function receives a copy, so any modifications inside the function stay local.
func reset(nums [3]int) {
nums[0] = 0
nums[1] = 0
nums[2] = 0
fmt.Println("inside reset:", nums) // [0 0 0]
}
func main() {
vals := [3]int{10, 20, 30}
reset(vals)
fmt.Println("after reset:", vals) // [10 20 30]
}
Copying large arrays is expensive:
Passing a large array by value creates a full copy of every element. For big arrays this costs time and memory. Slices avoid this by carrying a reference to an underlying array, which is one reason slices are more common in Go code.
Getting the Length of an Array
The built‑in len function returns the number of elements in an array.
func main() {
var a [7]int
fmt.Println(len(a)) // Output: 7
days := [7]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
fmt.Println(len(days)) // Output: 7
}
Because the length is known at compile time, len on an array is often evaluated at compile time as well. There is no runtime lookup — the compiler already has the answer.
Memory Layout and Performance Implications
An array in Go is stored as a single contiguous block of memory. There are no pointers, no headers, no indirection — just the elements packed back‑to‑back. When the compiler sees [4]int, it allocates exactly 4 × sizeof(int) bytes.
This simplicity yields several practical benefits:
- Cache friendliness: All elements sit in one line of memory, so the CPU prefetcher does its job well.
- No heap allocations: Small arrays can live entirely on the stack, avoiding garbage collection pressure.
- Predictable size: The runtime never needs to consult metadata to find the length; the type itself encodes it.
These characteristics make arrays a solid choice for small, fixed‑size collections where the number of items is known ahead of time — like a checksum, a color RGB triple, or a set of configuration flags.
When to Use Arrays Instead of Slices
Slices are more common in Go because they’re flexible, but arrays still have their place. Use an array when:
- The size is a fixed constant that will never change (e.g.,
[32]bytefor a SHA‑256 hash). - You need a value type that can be compared with
==. Two arrays of the same type are comparable if their element type is comparable. - You want the compiler to enforce a precise memory footprint at the type level — for example, when interfacing with C libraries or network protocols.
If you ever think “I might need to add more items later,” a slice is the right tool.
Arrays as constant‑size containers:
A [32]byte for a hash output is a classic example where arrays shine. The size is baked into the algorithm, and the compiler ensures no other length can slip in.
Summary
An array declaration in Go creates a fixed‑size, homogeneous, value‑type collection. The length is fused into the type, which prevents size mismatches at compile time and gives the compiler precise control over memory layout. Arrays start life fully zeroed, guaranteeing no uninitialized reads. Assignment and function passing copy the entire array — a deliberate trade‑off that favours simplicity and safety over flexibility.