On this page

Slices and Maps

14 min read TextCh. 3 — Data and Errors

Arrays, Slices and Maps: Go's Collections

Go has three fundamental collection types: arrays (rarely used directly), slices (the most-used collection), and maps (statically-typed hash tables). Understanding how they work internally is key to writing efficient, bug-free Go code.

Arrays: Fixed-Size Collections

An array in Go has a fixed size that is part of its type. [3]int and [5]int are completely different types:

// Declaration
var a [5]int                    // [0 0 0 0 0]
b := [3]string{"a", "b", "c"}  // [a b c]
c := [...]int{1, 2, 3, 4, 5}  // size inferred: [5]int

// Access
fmt.Println(b[0])  // "a"
b[1] = "z"

// Iterate
for i, v := range c {
    fmt.Printf("c[%d] = %d\n", i, v)
}

Arrays are value types: assigning them or passing them as parameters makes a complete copy. For this reason, in practice you almost always use slices.

Slices: Go's Dynamic Collection

A slice is a view over an underlying array. It has three properties:

  • Pointer: to the first element of the underlying array
  • Length (len): number of accessible elements
  • Capacity (cap): number of elements from the pointer to the end of the underlying array
// Several ways to create a slice
s1 := []int{1, 2, 3, 4, 5}      // literal
s2 := make([]int, 5)              // [0 0 0 0 0], len=5, cap=5
s3 := make([]int, 3, 10)          // [0 0 0], len=3, cap=10
var s4 []int                       // nil slice

fmt.Println(len(s1), cap(s1))  // 5 5
fmt.Println(len(s2), cap(s2))  // 5 5
fmt.Println(len(s3), cap(s3))  // 3 10
fmt.Println(s4 == nil)          // true

`append`: Adding Elements

append is the fundamental function for slices. If the capacity is sufficient, it adds to the same array. If not, it allocates a new, larger array (typically doubling the capacity) and copies the elements:

s := []int{1, 2, 3}
s = append(s, 4)          // [1 2 3 4]
s = append(s, 5, 6, 7)   // [1 2 3 4 5 6 7]

// Append another slice
other := []int{8, 9, 10}
s = append(s, other...)   // [1 2 3 4 5 6 7 8 9 10]

// Always assign the result of append
s = append(s, 11)  // NOT: append(s, 11) without assigning

Slicing: Creating Sub-slices

s := []int{0, 10, 20, 30, 40, 50}

// s[start:end] — end is exclusive
s1 := s[1:4]  // [10 20 30], len=3, cap=5
s2 := s[:3]   // [0 10 20], from the start
s3 := s[3:]   // [30 40 50], to the end
s4 := s[:]    // reference to the same underlying array

// Three-index slicing: control the capacity of the sub-slice
s5 := s[1:3:4]  // [10 20], len=2, cap=3

Caution: Sub-slices Share Memory

original := []int{1, 2, 3, 4, 5}
sub := original[1:3]  // [2 3], shares array with original

sub[0] = 99
fmt.Println(original)  // [1 99 3 4 5] — original was modified!

// To avoid this, use copy
clone := make([]int, 2)
copy(clone, original[1:3])
clone[0] = 99
fmt.Println(original)  // [1 2 3 4 5] — unchanged

`copy`: Copying Elements Between Slices

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)

n := copy(dst, src)  // copies min(len(dst), len(src)) elements
fmt.Println(n, dst)  // 3 [1 2 3]

// copy also works with strings to []byte
bs := make([]byte, 5)
n = copy(bs, "hello")
fmt.Println(n, bs)  // 5 [104 101 108 108 111]

Removing Elements from a Slice

Go has no delete function for slices. The standard pattern uses append:

s := []int{1, 2, 3, 4, 5}
i := 2  // index to remove

// Remove element at index i (without preserving order)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// [1 2 5 4]

// Remove element at index i (preserving order)
s = append(s[:i], s[i+1:]...)
// Caution: this shares memory with the original if there are other sub-slices

Common Slice Patterns

// Stack with a slice
stack := []int{}
stack = append(stack, 1, 2, 3)  // push
top := stack[len(stack)-1]       // peek
stack = stack[:len(stack)-1]     // pop

// Filter elements
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var evens []int
for _, n := range nums {
    if n%2 == 0 {
        evens = append(evens, n)
    }
}

Maps: Typed Hash Tables

A map is an unordered collection of key-value pairs with static types for both:

// Create a map
m1 := map[string]int{
    "one":   1,
    "two":   2,
    "three": 3,
}

m2 := make(map[string]int)     // empty map, ready to use
var m3 map[string]int           // nil map — cannot write to it

// Basic operations
m2["key"] = 42               // write
value := m2["key"]           // read (zero value if not present)
delete(m2, "key")            // remove
fmt.Println(len(m2))         // number of pairs

The "Comma Ok" Idiom

When reading from a map, always use the "comma ok" idiom to distinguish between "key does not exist" and "key exists with zero value":

scores := map[string]int{"Ana": 95, "Luis": 0}

// Without comma ok — cannot distinguish
a := scores["Ana"]    // 95
p := scores["Pedro"]  // 0 — zero value or actually 0?

// With comma ok — correct form
if val, ok := scores["Luis"]; ok {
    fmt.Printf("Luis exists with value %d\n", val)  // Luis exists with value 0
}

if _, ok := scores["Pedro"]; !ok {
    fmt.Println("Pedro is not in the map")
}

Iterating Over a Map

m := map[string]int{"a": 1, "b": 2, "c": 3}

// Iteration order is NOT guaranteed
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

// Keys only
for key := range m {
    fmt.Println(key)
}

// Sorted iteration: extract keys, sort, iterate
import "sort"

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Printf("%s: %d\n", k, m[k])
}

Maps with Struct Values

type Student struct {
    Name  string
    Grade float64
}

students := map[string]Student{
    "A001": {Name: "Ana", Grade: 9.5},
    "A002": {Name: "Luis", Grade: 8.7},
}

// To modify a field, you must replace the entire struct
student := students["A001"]
student.Grade = 10.0
students["A001"] = student

// Alternative: use *Student as value
studentPtrs := map[string]*Student{
    "A001": {Name: "Ana", Grade: 9.5},
}
studentPtrs["A001"].Grade = 10.0  // direct modification

nil vs. Empty: The Important Distinction

// Nil slice
var sNil []int
fmt.Println(sNil == nil)   // true
fmt.Println(len(sNil))     // 0 — safe
sNil = append(sNil, 1)     // safe — append handles nil

// Empty slice
sEmpty := []int{}
fmt.Println(sEmpty == nil) // false
fmt.Println(len(sEmpty))   // 0

// JSON: nil → null, empty → []
// Nil map
var mNil map[string]int
fmt.Println(mNil == nil)   // true
fmt.Println(len(mNil))     // 0 — safe
val := mNil["key"]         // 0 — safe
// mNil["key"] = 1         // PANIC! cannot write to nil map

// Empty map
mEmpty := make(map[string]int)  // or map[string]int{}
mEmpty["key"] = 1               // safe

With slices and maps mastered, in the next lesson we will explore pointers — how Go manages memory without the dangerous arithmetic of C.

Sub-slices share the underlying array
When you create a sub-slice (s2 := s[1:3]), s2 and s share the same underlying array. Modifying s2 modifies s too. If you need an independent copy, use copy(). This behavior is the source of many subtle bugs in Go.
nil vs. empty in slices and maps
A nil slice (var s []int) and an empty slice (s := []int{}) behave the same with len(), append() and range. The difference matters when serializing to JSON: nil becomes null, empty becomes []. For maps, reading from a nil map is safe (returns zero values), but writing to a nil map causes a panic.