Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last time, you used declarations, zero values, and conversions in your Go module. Now those ideas become important for collections: an uninitialized slice is useful in a different way from an uninitialized map, and assigning an array has a different effect from assigning either one.
This lesson is about choosing the right shape for your data. Allow about 45 minutes to watch the selected video segments, read the short sections of the Go documentation, and run the example in your existing module.
Arrays: a fixed size that belongs to the type
Suppose a program has exactly three status labels. An array expresses that fixed dimension:
statuses := [3]string{"new", "active", "done"}
fmt.Println(statuses[1]) // active
The type is [3]string, not merely “an array of strings.” [4]string is a different type. You can also let the compiler count the elements with [...]string{"new", "active", "done"}; the resulting type is still [3]string.
An array is a value containing its elements. Assigning one array to another copies those elements:
original := [2]int{10, 20}
separate := original
separate[0] = 99
fmt.Println(original) // [10 20]
fmt.Println(separate) // [99 20]
This is useful when the dimension is genuinely fixed and an independent copy is what you want. For a variable-length list—even one that currently happens to hold two items—a slice is usually the better choice.
Watch the introduction and array portion of NeuralNine’s Arrays, Slices, Maps - Go Programming Tutorial #3. Focus on how the number inside the array type determines its size, and how to access an element by its zero-based index.
Arrays, Slices, Maps - Go Programming Tutorial #3
NeuralNine introduces the three collection types, then demonstrates array declaration, initialization, and indexing.
Watch the overview for the distinction between sequences and key-value data. Continue with arrays; notice both the fixed size and the zero values of elements in an array declared without an initializer.
The official Go blog makes the assignment rule particularly clear:
Go Slices: usage and internals - The Go Programming Language
Read Andrew Gerrand’s explanation of arrays on the Go blog to connect fixed-size syntax with value-copying behavior.
Under “Arrays,” begin at the paragraph opening “An array type definition specifies a length and an element type.” Read the array explanation, then look at the two array literals immediately below it, ending with the statement that both give b the same array type. Pay particular attention to the paragraph beginning “Go’s arrays are values.”
Slices: sequences with flexible length and shared storage
A slice has type []T, with no length in the type. It is the usual choice for a sequence whose length may change:
guests := []string{"guest1", "guest2"}
guests = append(guests, "guest3")
fmt.Println(guests) // [guest1 guest2 guest3]
append returns a new slice value, so retain that result. The elements may still occupy the same storage, or append may allocate new storage if more room is needed. Code should not depend on which happens.
A slice describes a portion of an underlying array using a starting position, a length, and a capacity. Length is the number of elements you can currently index. Capacity is how far that slice could extend within its underlying storage without needing a new array.
numbers := make([]int, 0, 3) // length 0, capacity 3
numbers = append(numbers, 10) // length 1
Although numbers initially has capacity three, numbers[0] is invalid before the append: indexing is limited by length, not capacity. By contrast, make([]int, 3) starts with length three, containing three zero-valued integers.
Watch the slice segment of the same video for the everyday creation, append, copy, and slicing syntax. The blog reading that follows explains why those operations behave as they do.
Arrays, Slices, Maps - Go Programming Tutorial #3
NeuralNine demonstrates slices as variable-length sequences, including make, append, copy, and slice expressions.
Watch the slice segment. Track which operations change an existing element and which return a slice value that must be retained.
Go Slices: usage and internals - The Go Programming Language
Return to Andrew Gerrand’s Go blog article for the relationship between slices and their underlying arrays.
Under “Slices,” read slice creation, including the examples using literals and make. Continue from the paragraph beginning “A slice can also be formed” through “Slice internals”: slicing and storage. Finally, under “Growing slices (the copy and append functions),” read copy basics. Focus on what is shared after slicing and what copy duplicates.
A slice expression uses a half-open range: base[1:3] includes positions 1 and 2, but not 3. It creates another slice referring to the same elements:
base := []int{10, 20, 30, 40}
middle := base[1:3] // [20 30]
middle[0] = 99
fmt.Println(base) // [10 99 30 40]
The image below shows several slice values referring to portions of underlying arrays. For example, s0 and s1 begin at the same element of array a; other slices begin farther into a and have different lengths and capacities. The separate array at the bottom illustrates why you must distinguish another view of the same data from independent data.

Assigning a slice copies its descriptor, not its elements. If you need an independent sequence, allocate a destination and copy into it:
snapshot := make([]int, len(base))
copy(snapshot, base)
A nil slice is often a convenient starting point:
var items []string // nil; len(items) and cap(items) are both 0
items = append(items, "first")
You can append to it without calling make first. You cannot index it until it contains an element.
Maps: look up values by key, not position
A slice answers “what is at position 2?” A map answers a different question, such as “how many times have I seen this guest label?” Its type states both the key and value types:
counts := make(map[string]int)
counts["guest1"]++
counts["guest1"]++
fmt.Println(counts["guest1"]) // 2
The first increment works because reading a missing key produces the value type’s zero value: here, 0. Maps also support literals, such as map[string]int{"guest1": 2}.
There is one crucial difference from nil slices:
var missing map[string]int // nil
fmt.Println(missing["guest1"]) // 0: reading is allowed
// missing["guest1"] = 1 // panic: cannot write to a nil map
Initialize a map with make or a literal before inserting entries. Assigning an initialized map to another variable does not make an independent copy of its entries; changes through either variable are visible through the other.
Watch the map segment of the video, especially the check for whether a key exists.
Arrays, Slices, Maps - Go Programming Tutorial #3
NeuralNine demonstrates creating a map, adding and retrieving entries, checking whether a key exists, and deleting an entry.
Watch the map segment. Contrast looking up an absent key with explicitly asking whether that key is present.
Read the “Maps” subsection of Effective Go for the precise lookup rule and the value, ok form.
The official Effective Go guide explains map keys, missing-key results, presence checks, and deletion.
In the “Maps” subsection, start at “Maps are a convenient and powerful” and read the map guidance, stopping before “Printing.” Focus on the example where 0 might mean either a stored value or a missing key, and on how the second lookup result resolves that ambiguity.
When zero is a meaningful stored value, use the second result:
counts["guest2"] = 0
value, ok := counts["guest2"] // 0, true
other, found := counts["none"] // 0, false
fmt.Println(value, ok, other, found)
delete(counts, "guest2") removes an entry; deleting an absent key is safe. len(counts) gives the number of entries, but maps have no cap operation. A map’s keys must be comparable: strings and integers work, as do arrays whose element types are comparable; slices cannot be map keys. Maps also do not promise an iteration order, so choose a slice when position or order is part of what the data means.
Make the choice in your module
The distinction is about intent, not just available syntax:
| If you need… | Choose… | Important consequence |
|---|---|---|
| Exactly a specified number of elements | Array, [N]T |
Assignment copies its elements; size is part of its type. |
| An ordered sequence that may grow | Slice, []T |
Slices may share elements; retain the result of append. |
| Values associated with keys | Map, map[K]V |
Check presence when a stored zero value differs from an absent key. |
To see all three in your existing greeter module, leave its tested greet function intact. Add a file named collections_demo.go beside main.go:
package main
import "fmt"
func showCollections() {
statuses := [3]string{"new", "active", "done"}
statusCopy := statuses
statusCopy[0] = "queued"
fmt.Println("original status:", statuses[0]) // new
var guests []string
guests = append(guests, "guest1", "guest2", "guest1")
snapshot := make([]string, len(guests))
copy(snapshot, guests)
firstTwo := guests[:2]
firstTwo[0] = "guest3"
fmt.Println("guests:", guests) // [guest3 guest2 guest1]
fmt.Println("snapshot:", snapshot) // [guest1 guest2 guest1]
counts := make(map[string]int)
for _, guest := range guests {
counts[guest]++
}
counts["waiting"] = 0
waiting, present := counts["waiting"]
absent, found := counts["absent"]
fmt.Println("waiting:", waiting, present) // 0 true
fmt.Println("absent:", absent, found) // 0 false
fmt.Println("guest3 count:", counts["guest3"]) // 1
}
Add showCollections() near the end of your existing main function, then run:
go fmt ./...
go test ./...
go run .
The for _, guest := range guests line visits each slice element; _ discards the position because this code only needs the value. Notice the three different outcomes: changing statusCopy leaves the array statuses alone; changing firstTwo changes guests because they share elements; changing firstTwo does not change snapshot because copy duplicated its elements. The map then counts the resulting guest labels without requiring you to search the slice repeatedly.
Takeaways
Choose an array when the dimension is fixed and value-copying suits the job; choose a slice for most ordered, variable-length sequences; choose a map for key-based lookup. Remember the operational differences: slices can share underlying elements, append returns a slice to retain, and a nil map can be read but must be initialized before writing.
Next, you will write functions using multiple return values, named function types, and variadic parameters. The slice operations here have already introduced one important function-design idea: when an operation may produce a new slice value, its caller needs that value back.