Collections: Arrays, Slices, and Maps
Go has three main collection types: arrays (fixed-size), slices (dynamic), and maps (key-value). Slices are by far the most commonly used.
Arrays
Arrays have a fixed size that's part of the type. [3]int and [5]int are different types.
Declaring Arrays
// Declaration with zero values
var arr [5]int // [0, 0, 0, 0, 0]
// Declaration with initialization
arr := [5]int{1, 2, 3, 4, 5}
// Let compiler count
arr := [...]int{1, 2, 3, 4, 5} // Size is 5
// Specific indices
arr := [5]int{0: 10, 4: 50} // [10, 0, 0, 0, 50]
Working with Arrays
arr := [5]int{1, 2, 3, 4, 5}
// Access elements
fmt.Println(arr[0]) // 1
fmt.Println(arr[4]) // 5
// Modify elements
arr[0] = 10
// Length
len(arr) // 5
// Iterate
for i, v := range arr {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}
// Arrays are values (copied on assignment)
arr2 := arr // Creates a copy
arr2[0] = 100 // Doesn't affect arr
Multidimensional Arrays
// 2D array (3 rows, 4 columns)
var matrix [3][4]int
// With initialization
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// Access
matrix[0][1] // 2
Arrays are rarely used directly. Slices are preferred for most use cases.
Slices
Slices are dynamic, flexible views into arrays. They're the workhorse of Go collections.
Creating Slices
// From array
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2, 3, 4]
// Slice literal (creates underlying array)
slice := []int{1, 2, 3, 4, 5}
// Using make (length, optional capacity)
slice := make([]int, 5) // len=5, cap=5, [0,0,0,0,0]
slice := make([]int, 3, 10) // len=3, cap=10
// Nil slice
var slice []int // nil, len=0, cap=0
Slice Internals
A slice is a struct with three fields:
- Pointer: to the first element
- Length: number of elements
- Capacity: max elements before reallocation
slice := make([]int, 3, 5)
len(slice) // 3
cap(slice) // 5
Slicing Syntax
arr := [5]int{1, 2, 3, 4, 5}
arr[1:4] // [2, 3, 4] - elements 1 to 3
arr[:3] // [1, 2, 3] - first 3 elements
arr[2:] // [3, 4, 5] - from index 2 to end
arr[:] // [1, 2, 3, 4, 5] - entire array
// Full slice expression (with capacity)
arr[1:3:4] // [2, 3] with cap=3 (4-1)
Appending to Slices
slice := []int{1, 2, 3}
// Append single element
slice = append(slice, 4) // [1, 2, 3, 4]
// Append multiple elements
slice = append(slice, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// Append another slice
other := []int{8, 9, 10}
slice = append(slice, other...) // Spread operator
Important: append may return a new slice if capacity is exceeded:
slice := make([]int, 3, 3)
slice[0], slice[1], slice[2] = 1, 2, 3
// Must assign result back!
slice = append(slice, 4) // New underlying array allocated
Copying Slices
src := []int{1, 2, 3}
dst := make([]int, len(src))
copied := copy(dst, src) // Returns number of elements copied
// dst is now [1, 2, 3]
// Copy to smaller slice
small := make([]int, 2)
copy(small, src) // small is [1, 2]
Common Slice Operations
// Remove element at index i
slice = append(slice[:i], slice[i+1:]...)
// Insert element at index i
slice = append(slice[:i], append([]int{value}, slice[i:]...)...)
// Prepend
slice = append([]int{value}, slice...)
// Pop last element
last := slice[len(slice)-1]
slice = slice[:len(slice)-1]
// Clear slice (keep capacity)
slice = slice[:0]
// Check if empty
if len(slice) == 0 {
// empty
}
Slice Gotchas
// Slices share underlying array!
arr := []int{1, 2, 3, 4, 5}
slice1 := arr[1:4] // [2, 3, 4]
slice2 := arr[2:5] // [3, 4, 5]
slice1[1] = 100 // Modifies arr[2]
// arr is now [1, 2, 100, 4, 5]
// slice2 is now [100, 4, 5]
// To avoid, make a copy
slice1 := make([]int, 3)
copy(slice1, arr[1:4])
Nil vs Empty Slice
var nilSlice []int // nil
emptySlice := []int{} // not nil, but empty
emptyMake := make([]int, 0) // not nil, but empty
// All have len 0, but nilSlice == nil is true
// Behavior is usually the same
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
// Both work with append
nilSlice = append(nilSlice, 1) // Works!
emptySlice = append(emptySlice, 1) // Works!
Maps
Maps are hash tables: unordered key-value collections.
Creating Maps
// Using make
m := make(map[string]int)
// Map literal
m := map[string]int{
"alice": 25,
"bob": 30,
}
// Empty map literal
m := map[string]int{}
// Nil map (can read, but writes panic!)
var m map[string]int // nil
Basic Operations
m := make(map[string]int)
// Set values
m["alice"] = 25
m["bob"] = 30
// Get values
age := m["alice"] // 25
// Missing key returns zero value
age := m["unknown"] // 0
// Check if key exists
age, exists := m["alice"]
if exists {
fmt.Println("Alice is", age)
}
// Common pattern
if age, ok := m["alice"]; ok {
fmt.Println("Found:", age)
}
// Delete
delete(m, "alice")
// Length
len(m) // Number of key-value pairs
Iterating Maps
m := map[string]int{"a": 1, "b": 2, "c": 3}
// Iterate over key-value pairs
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// Keys only
for key := range m {
fmt.Println(key)
}
// Note: iteration order is random!
Map with Struct Values
type Person struct {
Name string
Age int
}
people := map[string]Person{
"alice": {Name: "Alice", Age: 25},
"bob": {Name: "Bob", Age: 30},
}
// Access
alice := people["alice"]
fmt.Println(alice.Name)
// Cannot modify struct fields directly
// people["alice"].Age = 26 // ERROR!
// Must reassign entire struct
alice := people["alice"]
alice.Age = 26
people["alice"] = alice
// Or use pointer values
peoplePtr := map[string]*Person{
"alice": {Name: "Alice", Age: 25},
}
peoplePtr["alice"].Age = 26 // Works!
Maps as Sets
Go doesn't have a built-in set type. Use a map with bool or empty struct:
// Using bool
seen := make(map[string]bool)
seen["apple"] = true
if seen["apple"] {
fmt.Println("Already seen apple")
}
// Using empty struct (more memory efficient)
type void struct{}
set := make(map[string]void)
set["apple"] = void{}
if _, exists := set["apple"]; exists {
fmt.Println("Already in set")
}
Map Key Requirements
Keys must be comparable (support ==):
- ✅ Strings, numbers, bools, pointers, arrays, structs (if fields are comparable)
- ❌ Slices, maps, functions
// Valid
map[string]int{}
map[[2]int]string{}
map[Point]string{} // Point{X, Y int}
// Invalid
// map[[]int]string{} // Slices can't be keys
Generics with Collections (Go 1.18+)
// Generic function for any slice type
func Filter[T any](slice []T, test func(T) bool) []T {
var result []T
for _, v := range slice {
if test(v) {
result = append(result, v)
}
}
return result
}
// Usage
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
// Generic map function
func Map[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
// Usage
doubled := Map(nums, func(n int) int { return n * 2 })
Common Patterns
Filter In Place
func filterInPlace(slice []int, keep func(int) bool) []int {
n := 0
for _, v := range slice {
if keep(v) {
slice[n] = v
n++
}
}
return slice[:n]
}
Deduplicate Slice
func unique(slice []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(slice))
for _, v := range slice {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
Reverse Slice
func reverse(slice []int) {
for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}
}
Group By
func groupBy(people []Person, keyFunc func(Person) string) map[string][]Person {
result := make(map[string][]Person)
for _, p := range people {
key := keyFunc(p)
result[key] = append(result[key], p)
}
return result
}
// Usage: group by age bracket
byAgeGroup := groupBy(people, func(p Person) string {
if p.Age < 30 { return "young" }
return "adult"
})
Next Steps
Continue to 06-structs-methods.md to learn about structs and methods.