Array Literals in Go

A comprehensive guide to using array literals for initializing arrays in Go, covering explicit size, indexed initialization, ellipsis inference, and partial assignment with zero values.

An array literal is the shortest way to create an array and give it values at the same time. Instead of declaring a zero-filled array and assigning each slot separately, you write the values between curly braces right after the array type. That single line becomes the array’s definition and its content.

What an Array Literal Does

An array literal is a form of composite literal — a syntax Go uses to build values for arrays, slices, maps, and structs directly from their elements. For arrays, the literal combines the type (which includes the element type and the fixed length) with a curly‑brace list of values. The compiler uses that information to allocate the array and populate it before your code ever reaches the assignment.

You will use array literals whenever you know the exact set of values the array should hold at compile time — configuration tables, lookup arrays, fixed translation maps, or test data. The alternative is to declare a zero‑valued array and then write several lines of index assignment, which quickly becomes repetitive.

Array literals are composite literals:

The Go specification groups array, slice, map, and struct literals under the same “composite literal” term. The rules about eliding inner types and partial values are shared across all of them, so what you learn here transfers to those types as well.

Basic Array Literal Syntax

The most direct form writes the full array type followed by the values in braces. The number of values must not exceed the declared length.

package main
import "fmt"
func main() {
    // Explicit size with all elements
    primes := [5]int{2, 3, 5, 7, 11}
    fmt.Println(primes)
}
[2 3 5 7 11]

The variable primes has type [5]int — an array of exactly five integers. The literal provides one value per position, in order. You cannot accidentally provide six elements; the compiler will stop you.

This style works when you know both the length and the values ahead of time, such as defining the set of error codes your package exports or hard‑coding a small lookup table.

Partial Initialization and Zero Values

You can supply fewer values than the array’s length. Every position you leave out is filled with the zero value of the element type — 0 for numbers, "" for strings, false for bools.

vals := [5]int{10, 20, 30}
fmt.Println(vals)
[10 20 30 0 0]

Positions 3 and 4 are implicitly 0. This is useful when an array is mostly zero and you only need to set a handful of slots explicitly.

Partial literals must still match the array type:

The array size in the literal type is mandatory. vals := [5]int{10, 20, 30} works because the literal declares a [5]int. Writing vals := [3]int{10, 20, 30, 0, 0} would be a compile error — too many elements for the declared length.

Indexed Array Literals

Sometimes the values you need are scattered across a large array. Instead of writing out dozens of zeroes, you can tag each value with its index using the index: value syntax inside the braces.

// Array of size 5 with values at specific indices
flags := [5]bool{0: true, 4: true}
fmt.Println(flags)
[true false false false true]

Index 0 gets true, index 4 gets true, and the remaining indices become false (the zero value for bool). The index‑value pairs can appear in any order — the compiler sorts them out — but each index must lie within the bounds of the array.

This pattern shines when you are mapping integer constants to strings or building a sparse lookup table where only a few slots are meaningful. It keeps the literal readable even for arrays with hundreds of elements.

Indexed literals are the clean way to build sparse arrays:

Instead of filling an array with placeholder zeroes, an indexed literal shows exactly which positions are interesting. A future reader can see at a glance that only positions 0 and 4 matter, without scanning through dozens of false entries.

Combining Positional and Indexed Elements

The Go compiler allows mixing positional values and indexed entries within the same literal. The positional values occupy the next available indices that are not explicitly claimed.

mixed := [...]int{1, 2, 4: 400, 500}
fmt.Println(mixed)
[1 2 0 0 400 500]

Here is what happens step by step:

  • Positional 1 goes to index 0.
  • Positional 2 goes to index 1.
  • 4: 400 explicitly sets index 4.
  • The final positional 500 goes to the next available slot after the last explicit index — index 5.

Indices 2 and 3 receive the zero value 0 because nothing was assigned to them. This mixing is legal but requires care: a reader might miss that indices jump from 1 to 4.

Using ... to Let the Compiler Count Elements

When you do not want to manually count the number of values, replace the length with .... The compiler counts the highest referenced index (plus one) and sets the array length to that number.

// Length inferred as 4
colors := [...]string{"red", "green", "blue", "yellow"}
fmt.Printf("len=%d %v\n", len(colors), colors)
len=4 [red green blue yellow]

The array colors has type [4]string. If you later add a fifth color to the literal, the length automatically becomes 5 — no manual adjustment needed.

... also works with indexed literals. The length becomes the highest explicit index plus one.

b := [...]int{1, 3: 9, 5: 25}
fmt.Println(len(b), b)
6 [1 0 0 9 0 25]

The highest index is 5, so the array gets length 6. The compiler inserts zeroes for indices 1, 2, and 4.

... still produces a fixed-size array:

Do not mistake [...] for a slice. The resulting array has a fixed length determined at compile time. You cannot append to it, and passing it to a function that expects []T will fail. The array is still an array, just with an inferred size.

Array Literals Versus Slice Literals

A common point of confusion is that removing the length from an array literal — or swapping ... for nothing — changes the type entirely.

arr := [...]int{1, 2, 3}  // [3]int — an array
slc := []int{1, 2, 3}     // []int — a slice

The first creates an array and then may immediately build a slice that references it if you use the slice literal syntax []int{...}. The second creates an array as the backing store for the slice, but the variable slc is a slice header, not the array itself.

The practical consequence: you can pass slc to functions like append, and its size can grow. arr cannot grow. If you try to pass arr where a []int is expected, the compiler will refuse with a type mismatch.

// This will not compile
var nums []int = [...]int{1, 2, 3}
// cannot use [...]int{...} as []int value in assignment

Remember: [n]T and []T are different types. The length — or the absence of one — is part of the type.

Common Mistakes with Array Literals

Mistakes around array literals usually stem from one of two misunderstandings: thinking the length does not matter, or confusing arrays with slices.

  • Assigning an array literal to a differently‑sized variable. The type [4]int is not [5]int. A literal with five elements can only be assigned to a variable of type [5]int (or inferred with [...]).

    var a [4]int = [5]int{1, 2, 3, 4, 5} // compile error
    
  • Trying to use append on an array. append only works on slices. An array literal produces an array, even with ....

  • Forgetting that partial literals zero‑fill. If you intentionally leave slots empty, the zero value still appears in the array, which might conflict with “empty” sentinel values like 0 or "". Use a separate mechanism if zero is a valid data value.

  • Assuming [...] creates a dynamic container. It does not. The size is fixed at compile time and cannot change at runtime.

  • Mixing positional and indexed entries without careful ordering. The next positional element always follows the last explicit index, not the last positional index. If your explicit index is high, the positional values will create a much larger array than expected.

Check the final index when mixing styles:

A positional value placed after 10: something will sit at index 11. If you only expected a few elements, the array length may silently become 12 or more. Always verify the inferred length with len() if you mix styles.

Practical Use: Building Lookup Tables

One of the strongest use‑cases for indexed array literals is building a fast, fixed‑size lookup table keyed by an integer constant.

const (
    StatusOK       = 0
    StatusNotFound = 1
    StatusError    = 2
)
var messages = [...]string{
    StatusOK:       "success",
    StatusNotFound: "not found",
    StatusError:    "internal error",
}
func statusMessage(code int) string {
    if code >= 0 && code < len(messages) {
        return messages[code]
    }
    return "unknown"
}

If you add a new status code and its message to the literal, the array grows automatically. The lookup is an O(1) index operation — no map overhead. This pattern is common in HTTP status‑line libraries, protocol parsers, and CLI exit‑code help text.

Summary

Array literals give you a single‑line, compiler‑checked way to create and populate arrays. You can spell out every element, fill only the meaningful positions with indexed syntax, or let the compiler count the size. The result is always a fixed‑size array, not a slice, and the array’s length is burned into its type.