Arrays, slices, and maps
Fixed-size arrays, dynamic slices with make and append, and key-value maps — Go's built-in collection types.
- Declare and use fixed-size arrays with [n]T syntax
- Create and grow slices using make and append
- Explain the relationship between a slice's length and capacity
- Declare, populate, and query a map with map[K]V
- Delete a key from a map and check whether a key exists
Every program eventually needs to work with collections of data — lists of items, lookups by name, sequences to iterate. Go's built-in collection types are lean but powerful. There are three to learn at the beginner level: arrays (fixed size, rarely used directly), slices (the workhorse of Go programs), and maps (key-value lookup tables). Understanding slices well is one of the highest-leverage things you can do early in Go.
Arrays
An array is a fixed-size sequence of elements of a single type. The size is part of the type:
var scores [3]int // [0, 0, 0] — zero-valued
scores[0] = 42
scores[1] = 85
scores[2] = 91
fmt.Println(scores) // [42 85 91]
fmt.Println(len(scores)) // 3An array literal initialises all elements at once:
primes := [5]int{2, 3, 5, 7, 11}Use ... to let the compiler count the elements:
primes := [...]int{2, 3, 5, 7, 11} // still [5]intArrays are values, not references. When you assign an array to a new variable or pass it to a function, Go copies all the elements. For large arrays this is expensive. In practice, most Go code uses slices instead of arrays directly. Arrays are the foundation that slices are built on.
Slices
A slice is a dynamically-sized view into an underlying array. The type is []T (no number in the brackets). You'll use slices constantly.
Slice literals
fruits := []string{"apple", "banana", "cherry"}
fmt.Println(fruits[0]) // apple
fmt.Println(len(fruits)) // 3make — create with a given length and capacity
make([]T, length, capacity) allocates a slice with the specified length and optional capacity:
s := make([]int, 3) // [0, 0, 0], len=3, cap=3
s := make([]int, 3, 10) // [0, 0, 0], len=3, cap=10Length is the number of elements currently in the slice. Capacity is the size of the underlying array — how many elements can be added before a reallocation is needed.
append — growing a slice
append adds elements to a slice and returns the new slice:
s := []int{1, 2, 3}
s = append(s, 4)
s = append(s, 5, 6, 7) // append multiple at once
fmt.Println(s) // [1 2 3 4 5 6 7]Always assign the result of append back to the slice variable. When the slice exceeds its capacity, Go allocates a new, larger array and copies the data — the original slice variable would point at the old array if you don't reassign.
append returns a new slice header. The pattern s = append(s, val) is idiomatic for a reason — append may or may not allocate a new array underneath. If it does, the old variable still points at the old memory. Always write s = append(s, ...).
Slicing a slice
You can take a sub-slice with the [low:high] syntax, which is half-open ([low, high)):
s := []int{10, 20, 30, 40, 50}
fmt.Println(s[1:3]) // [20 30] (indices 1 and 2)
fmt.Println(s[:2]) // [10 20] (from the start)
fmt.Println(s[3:]) // [40 50] (to the end)Sub-slices share the underlying array with the original. Modifying elements through a sub-slice modifies the original data. Use copy if you need an independent copy.
len and cap
s := make([]int, 3, 8)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 8len is how many elements are accessible. cap is how far the underlying array extends. You rarely need cap at the beginner level — len is what you use for bounds and loops.
Maps
A map is an unordered key-value store. The type is map[KeyType]ValueType:
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
fmt.Println(ages["Alice"]) // 30make for maps
Use make to create an empty map:
config := make(map[string]string)
config["host"] = "localhost"
config["port"] = "8080"A map literal with no entries also works: config := map[string]string{}.
Adding, updating, and deleting
ages["Dave"] = 35 // add a new key
ages["Alice"] = 31 // update existing key
delete(ages, "Bob") // remove a keydelete is a built-in function. It does nothing if the key doesn't exist — no error.
Checking whether a key exists
Map lookup returns a zero value for missing keys, which can be ambiguous (is ages["Unknown"] zero because the key is missing, or because Alice is actually 0 years old?). Use the two-value form to distinguish:
age, ok := ages["Alice"]
if ok {
fmt.Println("Alice is", age)
} else {
fmt.Println("Alice not found")
}The second return value ok is true if the key was present, false if it wasn't. You'll see this comma-ok idiom in several other Go contexts (type assertions, channel receives).
Maps must be initialised before use. A map variable declared with var m map[string]int is nil — reading from it is safe (returns zero values) but writing to it panics. Always initialise with make or a map literal before storing anything.
Check your understanding
Knowledge check
- 1.Why do you write
s = append(s, val)instead of justappend(s, val)? - 2.What does
ages["Unknown"]return if "Unknown" is not in the mapages map[string]int? - 3.Writing to a nil map in Go causes a runtime panic.
Do it yourself
Write a program that counts word frequencies in a slice of strings:
words := []string{"go", "is", "fast", "go", "is", "simple", "go"}Use a map[string]int and a for range loop. After counting, iterate the map and print each word and its count. Run go run main.go and verify that "go" appears 3 times.
Where to go next
You know Go's three core collection types and how to use them idiomatically. Next: structs and methods — how to define your own data types and attach behaviour to them, which is Go's answer to classes.