Go Slices and Maps: The Core Collections

Go Slices and Maps: The Core Collections
In every application, you will eventually need to group data together. Whether it's a list of users, a queue of background jobs, or a lookup table for configuration, Go provides two incredibly optimized data structures for these tasks: Slices and Maps.
While Go does have a traditional "Array" type, you will rarely use it directly. Instead, you'll work with Slices — which provide the flexibility of a dynamic array while maintaining the performance of a contiguous memory block.
What Are Go Slices and Maps?
A Go slice is a dynamically-sized, ordered sequence of elements backed by a contiguous array. It consists of three fields — a pointer to the underlying array, a length, and a capacity — allowing it to grow automatically via append. A Go map is an unordered collection of key-value pairs implemented as a hash table, providing O(1) average-time reads and writes for fast lookups.
The Array vs. Slice Distinction
Working with Slices
Slices are the most commonly used collection in Go. They are designed to be passing-friendly and efficient.
Declaring and Initializing Slices
There are several ways to create a slice depending on whether you know the initial data or just the required size.
Slice Internals: Length vs. Capacity
A slice is actually a small structure containing three things: a pointer to the underlying array, the Length (current number of elements), and the Capacity (the maximum elements the underlying array can hold before reallocating).
Slice Mechanics
5Returns the current number of elements in the slice.
10Returns the maximum number of elements the slice can hold without reallocating memory.
s = append(s, v)Adds an element. If length exceeds capacity, Go automatically allocates a larger underlying array (usually 2x bigger).
High-Performance Lookups: Maps
A Map is Go's built-in hash table. It stores data as key-value pairs and provides O(1) average-time complexity for retrieval, making it ideal for lookups.
Declaring and Using Maps
Like slices, you should generally use the make() function to initialize a map to avoid runtime panics on nil maps.
| Task / Feature | Slices | Maps |
|---|---|---|
| Access Method | Index-based (0, 1, 2...) | Key-based (String, Int, etc.) |
| Order | Maintains insertion order | Unordered (Randomized iteration) |
| Performance | Fast linear scanning | Blazing fast key-value lookups |
Performance Tips
- Pre-allocate when possible: If you know you'll be storing 1,000 items, use
make([]int, 0, 1000). This prevents Go from having to resize and copy the array multiple times, which is a major performance boost. - Iterating over Maps is Random: Go intentionally randomizes the order of map iteration to prevent developers from relying on a specific order that might change as the map grows.
Copying Slices and Avoiding Shared State Bugs
One of the most common Go pitfalls is accidentally sharing the underlying array between two slices. When you assign one slice to another, both point to the same memory.
Always use copy() when you need an independent slice that should not affect the original.
Slicing a Slice
Go supports Python-style slice expressions to create sub-slices without allocating new memory:
This is extremely efficient but comes with the same sharing caveat. Use a full slice expression original[1:4:4] (three-index slice) to limit the capacity of sub and prevent accidental mutations through append.
Concurrent Map Access
Go maps are not safe for concurrent use. If multiple goroutines read and write the same map simultaneously without synchronization, the program will panic with a "concurrent map read and map write" error.
Alternatively, Go 1.9+ ships sync.Map in the standard library, which is optimized for read-heavy workloads with infrequent writes. The concurrency patterns behind this are explored in the Go goroutines and concurrency guide.
Sorting Slices
The sort package from the Go standard library provides efficient sorting for common types:
Since Go 1.21, the slices package in the standard library provides generic, type-safe sorting: slices.Sort(names).
Maps for Frequency Counting
A very common pattern in Go is using a map as a frequency counter or set:
The zero-value initialization means you never need to check if a key exists before incrementing its count — a unique and elegant Go idiom.
Understanding slices and maps is foundational before tackling the Go variables, types, and constants primer or moving into the Go standard library deep dive, where these collection types appear in almost every example. The Go team's official slice mechanics blog post provides a deep technical explanation of how the backing array growth algorithm works.
Next Steps
Now that you can manage collections of data, it's time to learn how to add behavior to that data. In our next tutorial, we will explore Methods and Interfaces, where you'll see how Go implements shared behavior and "Object Oriented" patterns without using classes or inheritance.
Common Mistakes with Go Slices and Maps
1. Mutating a slice passed to a function Slices are reference types — a function that appends to a slice parameter may or may not affect the caller's slice, depending on whether a reallocation occurs. To be safe, return the modified slice from the function rather than relying on in-place mutation.
2. Ranging over a map and expecting order
Go maps have intentionally randomised iteration order. Never rely on for k, v := range myMap to produce keys in insertion or alphabetical order. Sort the keys explicitly if you need deterministic output.
3. Forgetting to initialise a map before writing
var m map[string]int creates a nil map. Writing to it (m["key"] = 1) panics at runtime. Always initialise with m := make(map[string]int) or a map literal m := map[string]int{}.
4. Off-by-one errors with slice indices
A slice s[low:high] includes index low but excludes high. s[1:4] returns elements at indices 1, 2, and 3. Forgetting the exclusive upper bound is a frequent source of bugs. See the Go spec on slice expressions.
5. Copying a slice header vs deep copying
b := a copies the slice header (pointer, length, capacity) but not the underlying array. Modifying b[0] also changes a[0]. Use copy(b, a) or append([]T{}, a...) for a true independent copy.
Frequently Asked Questions
What is the difference between len and cap for slices?
len is the number of elements currently in the slice. cap is the total size of the underlying array starting from the slice's first element. Appending beyond cap triggers a reallocation and doubles capacity. Understanding this prevents unnecessary allocations in hot paths.
When should I use a slice vs an array? Arrays are fixed-size value types — rarely used directly in Go code. Slices are the idiomatic choice for sequences of any length. Arrays are useful when you need a fixed-size, stack-allocated buffer (e.g. cryptographic nonces or small lookup tables).
How do I check if a key exists in a map?
Use the two-return form: val, ok := m["key"]. If the key exists, ok is true. If not, ok is false and val is the zero value of the map's value type. The Go blog on maps covers this and other map patterns.
