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.
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.
In Go, an Array has a fixed length defined at compile time (e.g., [5]int). You cannot resize it. A Slice, however, is a descriptor for a segment of an underlying array. It can grow and shrink dynamically as your program runs.
1. The Slice Mirror: Header Anatomy
A Go slice is not an array; it is a Header that points to an array. This distinction is the most important concept in Go memory management.
The SliceHeader Physics
Inside the Go runtime, a slice is represented by a 24-byte structure (on 64-bit systems):
- Pointer (8 bytes): The memory address of the first element.
- Length (8 bytes): How many elements the slice currently contains.
- Capacity (8 bytes): How many elements the underlying array can hold before requiring a new memory allocation.
The Physics of append: When you append to a slice and exceed its capacity, Go allocates a new, larger array (usually 2x the size), copies the old data, and updates the pointer. This is a high-cost operation. In high-performance systems, we pre-allocate capacity to avoid this "Reallocation Spike."
2. 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.
func main() {
// 1. Literal declaration
fruits := []string{"Apple", "Banana", "Cherry"}
// 2. Using the make() function (Recommended for performance)
// Creates a slice of integers with length 5 and capacity 10
scores := make([]int, 5, 10)
// 3. Adding elements
fruits = append(fruits, "Dragonfruit")
fmt.Println(fruits)
}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
3. The Map Mirror: Hash Bucket Physics
Go maps are built as highly-optimized Hash Tables. Unlike slices, which are contiguous, maps are structured into "Buckets."
The Map Physics
- Hashing: Go uses a fast, hardware-accelerated hash function (like AES-NI on modern CPUs) to convert your key into a number.
- Buckets: This number determines which "Bucket" (a small 8-item array) the data lives in.
- Randomized Iteration: In 2026, Go still intentionally randomizes map iteration order. This is a security feature to prevent "Hash Flooding" attacks and an architectural hint to prevent programmers from relying on accidental implementation details.
4. 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.
func main() {
// Declaring a map where keys are strings and values are integers
userAges := make(map[string]int)
// Adding data
userAges["Alice"] = 30
userAges["Bob"] = 25
// Retrieving data with the "comma ok" idiom
age, exists := userAges["Charlie"]
if !exists {
fmt.Println("User not found!")
} else {
fmt.Println("Age:", age)
}
// Deleting data
delete(userAges, "Alice")
}| Task / Feature | Slices | Maps |
|---|---|---|
| No comparison data available | ||
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.
a := []int{1, 2, 3}
b := a // b shares the same underlying array as a
b[0] = 99
fmt.Println(a) // [99 2 3] — a was also modified!
// To create a true independent copy:
c := make([]int, len(a))
copy(c, a)
c[0] = 42
fmt.Println(a) // [99 2 3] — a is safeAlways 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:
original := []int{10, 20, 30, 40, 50}
sub := original[1:4] // [20, 30, 40] — shares original's backing arrayThis 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.
// Safe concurrent map using sync.RWMutex
var (
mu sync.RWMutex
cache = make(map[string]string)
)
func get(key string) (string, bool) {
mu.RLock()
defer mu.RUnlock()
val, ok := cache[key]
return val, ok
}
func set(key, value string) {
mu.Lock()
defer mu.Unlock()
cache[key] = value
}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:
import "sort"
names := []string{"Charlie", "Alice", "Bob"}
sort.Strings(names)
fmt.Println(names) // [Alice Bob Charlie]
scores := []int{42, 7, 100, 3}
sort.Ints(scores)
fmt.Println(scores) // [3 7 42 100]
// Custom sort for structs
users := []User{{Name: "Zara", Age: 30}, {Name: "Ana", Age: 25}}
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})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:
words := []string{"go", "is", "fast", "go", "is", "great"}
freq := make(map[string]int)
for _, word := range words {
freq[word]++ // Safe even if key doesn't exist yet (zero value is 0)
}
fmt.Println(freq) // map[fast:1 go:2 great:1 is:2]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.
Phase 4: Collection Mastery Checklist
- Verify Pre-Allocation: Audit your code for
appendloops and ensuremake([]T, 0, capacity)is used where the final size is predictable. - Audit Map Safety: Confirm that any map accessed by multiple Goroutines is protected by a
sync.RWMutexor uses thesync.Mapprimitive. - Implement Slicing Boundaries: Use the "Full Slice Expression"
s[i:j:k]to protect original arrays from unintended modifications viaappend. - Test Slice Equality: Remember that slices cannot be compared with
==. Usereflect.DeepEqualor custom loops for validation. - Use Zero-Length Maps: Ensure you check for the "Comma Ok" idiom (
val, ok := m[k]) to distinguish between a missing key and a zero-value entry.
Read next: Go Functions, Pointers, and Structs: The Behavior Mirror →
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.
