Often you need to store more than one value of the same type — a list of names, a sequence of scores, a buffer of incoming bytes. Go gives you two tools for this: arrays and slices.

In practice, you’ll use slices 99% of the time. But understanding arrays first makes slices much easier to grasp, because slices are built on top of arrays.

Arrays

An array is a fixed-size sequence of values of the same type. The size is part of the array’s type — [3]int and [5]int are completely different types.

package main

import "fmt"

func main() {
    var scores [3]int
    scores[0] = 95
    scores[1] = 88
    scores[2] = 73

    fmt.Println(scores)
    fmt.Println("First score:", scores[0])
    fmt.Println("Length:", len(scores))
}
go run main.go
[95 88 73]
First score: 95
Length: 3

Things to notice:

  • Index starts at 0, not 1. scores[0] is the first element. This is universal across programming languages.
  • len(scores) returns the array’s length. len is a built-in function you’ll use constantly.
  • Out-of-range access is an error. scores[3] would fail — if Go can see the index is out of range at compile time (like this literal 3), the compiler rejects it; otherwise it panics at runtime.

You can also declare and fill an array in one go:

// inside main()
fruits := [3]string{"apple", "banana", "cherry"}
fmt.Println(fruits)

Or let Go count for you with ...:

// inside main()
days := [...]string{"Mon", "Tue", "Wed", "Thu", "Fri"}
fmt.Println(len(days))   // 5

The problem with arrays

Arrays have a fixed size. If you want to add a sixth day, you can’t — the array’s type is [5]string. You’d need to create a new, bigger array and copy the values. That’s tedious. That’s why arrays are rare in everyday Go code, and slices are everywhere.

Slices

A slice is a flexible, growable view into an array. Slice types don’t include a size — []int is just “a slice of ints”, regardless of length.

package main

import "fmt"

func main() {
    scores := []int{95, 88, 73}

    fmt.Println(scores)
    fmt.Println("Length:", len(scores))
    fmt.Println("Capacity:", cap(scores))
}
[95 88 73]
Length: 3
Capacity: 3

Notice the empty [] instead of [3] — that’s the syntax for “slice, not array.”

Growing a slice with append

The most useful function in Go is append. It adds an element to a slice and returns a new slice:

// inside main()
scores := []int{95, 88, 73}
scores = append(scores, 100)
scores = append(scores, 64, 71)   // append takes any number of elements

fmt.Println(scores)
[95 88 73 100 64 71]

Always assign the result of append back to the slice. Sometimes append reuses the same underlying array, sometimes it allocates a bigger one — you don’t know which, so you must capture the return value.

Reading and modifying

Like arrays, slices use [index] to read and write:

// inside main()
scores := []int{95, 88, 73}
fmt.Println(scores[1])   // 88

scores[1] = 90
fmt.Println(scores)      // [95 90 73]

Slicing a slice

The slice’s namesake operation: take a portion of an existing slice using [low:high]. The result includes index low but excludes index high:

// inside main()
nums := []int{10, 20, 30, 40, 50}

fmt.Println(nums[1:4])   // [20 30 40] — indexes 1, 2, 3
fmt.Println(nums[:3])    // [10 20 30] — from start
fmt.Println(nums[2:])    // [30 40 50] — to end
fmt.Println(nums[:])     // [10 20 30 40 50] — everything

This is incredibly common. “Take the first 10 results”, “skip the first row”, “process from this point onwards” — all natural in Go.

Iterating with range

You saw this in the loops lesson, but it’s worth repeating in context:

// inside main()
nums := []int{10, 20, 30}

sum := 0
for _, n := range nums {
    sum += n
}
fmt.Println("Sum:", sum)
Sum: 60

Creating slices with make

When you know the size you want up front, make is more efficient than appending repeatedly:

// inside main()
buffer := make([]byte, 1024)   // length 1024, all zeros
fmt.Println("Length:", len(buffer))

You can also specify a separate capacity — the size of the underlying array — which lets you avoid reallocation as the slice grows:

// inside main()
results := make([]int, 0, 100)  // length 0, capacity 100
for i := 0; i < 100; i++ {
    results = append(results, i*i)
}

This is an optimization. For most code, []T{} or make([]T, 0) is fine.

A practical example

A complete program that reads a list of numbers, finds the sum and the largest:

package main

import "fmt"

func main() {
    nums := []int{12, 7, 25, 3, 19, 8, 31, 14}

    sum := 0
    largest := nums[0]

    for _, n := range nums {
        sum += n
        if n > largest {
            largest = n
        }
    }

    fmt.Println("Sum:    ", sum)
    fmt.Println("Largest:", largest)
    fmt.Println("Count:  ", len(nums))
}
Sum:     119
Largest: 31
Count:   8

Arrays vs slices, summarized

FeatureArraySlice
SizeFixed at compileGrows at runtime
Type[N]T[]T
Common useRareEverywhere
append worksNoYes

When you don’t know which to pick: use a slice. You’ll be right almost every time.

What’s next

Slices store values in order. But sometimes you don’t care about order — you want to look up a value by a name. That’s what maps are for.

Toggle theme (T)