Hello! In our last lesson, we covered the basics of declaring and using variables and constants in Go. You learned about Go's static type system, the convenience of the := operator, and the unique flexibility of untyped constants.
This lesson builds directly on that foundation. We will move from handling single pieces of data to managing collections. You'll learn to use Go's three built-in collection types: arrays, slices, and maps. Our focus will be on understanding "the Go way" of working with collections, particularly the memory model and performance implications that distinguish them from the dynamic lists, dictionaries, and arrays you're familiar with in Python and TypeScript.
Arrays: A Fixed Foundation
In Go, arrays are rigid, fixed-size collections where all elements must have the same type. The length of an array is part of its type, meaning [4]int and [5]int are two different, incompatible types. This is a significant departure from the dynamic arrays in TypeScript or lists in Python.
Let's watch a segment that introduces the built-in container types and then dives into the specifics of arrays.
Go Class: 05 Arrays, Slices, and Maps
This video from Matt KØDVB's "Go Class" provides a detailed look at Go's container types.
First, watch the introduction to get an overview of arrays, slices, and maps. Then, pay close attention to the section on arrays. Note the key properties: they are fixed-size, and importantly, they are value types. This means when you assign an array to another variable or pass it to a function, the entire array is copied.
As you saw, the fact that arrays are value types is crucial. This behavior ensures that functions receive an independent copy of the data, but it can be inefficient for large arrays. Because of this fixed size and copy behavior, you will rarely see raw arrays used in function signatures in idiomatic Go code. They serve as the rigid, underlying building block for Go's more flexible and powerful collection type: the slice.
Slices: The Idiomatic Workhorse
Slices are, by far, the more common and versatile way to work with sequences of data in Go. A slice is a lightweight data structure that provides a "view" into an underlying array. Think of it not as a container itself, but as a descriptor for a contiguous segment of an array.
This descriptor, often called a slice header, contains three components:
- A pointer to the first element of the underlying array that the slice can access.
- The length (
len) of the slice (the number of elements it contains). - The capacity (
cap) of the slice (the total number of elements in the underlying array, starting from the pointer).
The following video segment explains this descriptor model.
Go Class: 05 Arrays, Slices, and Maps
Let's continue with the same video to understand the slice descriptor.
Watch the segment from this timestamp. The key takeaway is that a slice is a small struct containing a pointer, length, and capacity. When you assign or pass a slice, you are only copying this small descriptor, not the large underlying array, making it very efficient. This also means multiple slices can point to the same underlying data.
The relationship between a slice and its underlying array, and the distinction between length and capacity, are fundamental. The image below provides a great visualization.

Because slices are just headers pointing to an array, modifying an element through one slice will be visible to any other slice that shares that part of the underlying array. This is a concept that will feel somewhat familiar given your experience with object references in TypeScript and Python, but Go's model gives you more explicit control and things to be aware of.
To get a more practical feel for how slices compare to the dynamic arrays you're used to, the following reading is highly relevant.
A JavaScript Developer's Guide to Go
This article by Prateek Surana provides direct comparisons between Go and JavaScript/TypeScript collections.
Please read the entire section titled "Arrays and Slices". Focus on how Go's slice behavior (modifying a slice affects the underlying array) differs from JavaScript's slice() method, which creates a shallow copy. Also, note the explanation of len, cap, and the append function.
Growing Slices with append
A slice's length is not fixed. You can grow a slice using the built-in append function. This is where capacity becomes important.
- If you append to a slice and there is still room in the underlying array (
len < cap),appendsimply uses that available space and returns a new slice header with an updated length. - If the underlying array is full (
len == cap),appendwill allocate a new, larger array, copy all the elements from the old array to the new one, and then add the new element. It returns a slice header pointing to this new array.
Because append might return a slice pointing to a completely different array, it is critical to always assign the result back to the same slice variable, using the idiom slice = append(slice, newValue).
Maps: Key-Value Pairs
Go's map type is its built-in hash table, used for storing key-value pairs. This will feel very similar to Python's dictionaries or TypeScript's Map objects.
Key features of Go maps:
- They are created using
make(map[KeyType]ValueType)or a map literal. - The zero value of a map is
nil. You can read from anilmap (which always yields the zero value for the value type), but attempting to write to anilmap will cause a runtime panic. - They are reference types. Like slices, when you pass a map to a function, you are passing a pointer to the underlying data structure, and modifications will be seen by the caller.
Let's watch a final segment from the video which covers maps in detail.
Go Class: 05 Arrays, Slices, and Maps
This segment explains the essentials of working with maps.
First, watch the introduction to maps. Pay close attention to the distinction between a nil map and an empty map created with make. This is a common source of bugs for new Go programmers. Next, watch the explanation of how to read from and write to maps, and how assignment works. Finally, focus on the "comma, ok" idiom from this section. This is the idiomatic Go way to safely check if a key exists in a map, distinguishing a missing key from a key whose value happens to be the zero value (e.g., 0 or "").
The "comma, ok" idiom is a pattern you'll see throughout Go. It's a clean and explicit way to handle operations that might not succeed or return a value.
Let's Write Some Code
Now, let's put these concepts into practice. The following code demonstrates creating and using a slice and a map to perform a common task: counting word frequencies. This example showcases iteration using the for...range construct, which is the standard way to loop over Go's collection types.
Replace the content of your main.go file with this code:
package main
import (
"fmt"
"strings"
)
func main() {
// Start with a slice of strings.
// This uses a slice literal to create the slice and its underlying array.
data := []string{"Go", "is", "a", "great", "language", "Go", "has", "simple", "syntax"}
fmt.Printf("Original slice: %v\n", data)
fmt.Printf("Length: %d, Capacity: %d\n\n", len(data), cap(data))
// Use 'append' to add a new element.
// The result must be assigned back to the slice variable.
data = append(data, "Go")
fmt.Printf("Slice after append: %v\n", data)
fmt.Printf("New Length: %d, New Capacity: %d\n\n", len(data), cap(data))
// Create a map to store word frequencies.
// We use 'make' to initialize it.
wordCounts := make(map[string]int)
// Use a 'for...range' loop to iterate over the slice.
// The 'range' keyword returns the index and the value for each element.
// We use the blank identifier '_' to ignore the index.
for _, word := range data {
// Convert to lowercase to count words case-insensitively.
lowerWord := strings.ToLower(word)
// Use the "comma ok" idiom to safely check for existence.
count, ok := wordCounts[lowerWord]
if ok {
// If the word exists, increment its count.
wordCounts[lowerWord] = count + 1
} else {
// If it's the first time we've seen the word, set its count to 1.
wordCounts[lowerWord] = 1
}
}
fmt.Println("Word Frequencies:")
// Iterate over the map to print the results.
// For maps, 'range' returns the key and the value.
for word, count := range wordCounts {
fmt.Printf(" - %s: %d\n", word, count)
}
}
Run this program with go run .. Observe the output. Notice how the length and potentially the capacity of the slice change after the append operation. See how the map is populated by iterating over the slice.
Conclusion
You've now covered the three fundamental collection types in Go and, more importantly, the design principles behind them.
Key Takeaways:
- Arrays are fixed-size value types. Their length is part of their type, and they are copied on assignment. They are the foundation but are used less frequently in public APIs.
- Slices are flexible, variable-size views into an underlying array. They are reference types (the descriptor is copied, but the pointer refers to the same data) and are the idiomatic choice for sequences. You must use
slice = append(slice, ...)to grow them safely. - Maps are reference-type hash tables for key-value storage. You must
makea map before writing to it, and you should use thevalue, okidiom to check for key existence. for...rangeis the universal tool for iterating over all collection types.
In our next lesson, we will formalize our understanding of program flow by exploring Go's control structures: if, for, and switch. With your knowledge of collections, you'll be able to write much more powerful and dynamic loops and conditional logic.