Slices & Maps

📋 Jump to Takeaways

Slices and maps are the data structures you'll reach for in every Go program. Slices are Go's dynamic arrays: flexible, efficient, and everywhere. Maps are your key-value store. Together they handle 90% of the data you'll ever work with.

Arrays

Arrays have a fixed size set at compile time. You'll rarely use them directly. Slices are almost always the better choice.

var a [3]int = [3]int{1, 2, 3}
b := [3]string{"go", "is", "fun"}
fmt.Println(a[0]) // 1
fmt.Println(len(b)) // 3

The size is part of the type. [3]int and [4]int are different types.

Slices

A slice is a dynamic, flexible view over an array. This is what you'll use 99% of the time.

s := []int{1, 2, 3}
fmt.Println(s[0]) // 1
fmt.Println(len(s)) // 3

What a Slice Really Is

A slice feels like a simple list, but it's actually a tiny struct with three fields:

// Roughly what Go uses internally
type slice struct {
    array unsafe.Pointer // pointer to the underlying array
    len   int            // how many elements you're using
    cap   int            // how many fit before a new array is needed
}

That's it. When you write []int{1, 2, 3}, Go allocates an array somewhere in memory and hands you back this little header pointing to it. The slice never holds the data itself.

This is why slices feel like references. Pass one to a function and Go copies the header, not the array. Both copies point to the same data.

func double(s []int) {
    for i := range s {
        s[i] *= 2
    }
}

nums := []int{1, 2, 3}
double(nums)
fmt.Println(nums) // [2 4 6], modified through the copy

When append runs out of room, Go quietly allocates a bigger array, copies everything over, and gives you a new header pointing to it. The old array becomes garbage. You never deal with any of this. Go roughly doubles the capacity each time.

s := make([]int, 0, 2) // cap=2
s = append(s, 1, 2)    // fits in existing array
s = append(s, 3)        // cap exceeded → new array, cap grows

And that's exactly why s = append(s, ...) needs the reassignment. If the array grew, your old header is pointing at stale memory.

When You Need a Pointer to a Slice

Mutating elements works fine across functions. Both headers point to the same array. But append is different. If a function appends, its local header gets the new len (and maybe a new array pointer), but the caller's header knows nothing about it.

func addItem(s []int, val int) {
    s = append(s, val) // caller never sees this
}

nums := []int{1, 2}
addItem(nums, 3)
fmt.Println(nums) // [1 2], unchanged

Pass a pointer to the slice so the function updates the caller's header directly:

func addItem(s *[]int, val int) {
    *s = append(*s, val)
}

nums := []int{1, 2}
addItem(&nums, 3)
fmt.Println(nums) // [1 2 3]

You can also just return the new slice (that's what append itself does). Reach for a pointer when you can't change the return type: callbacks, interface methods, or functions that already return an error.

make

Use make to create a slice with a specific length and optional capacity.

s := make([]int, 3)       // len=3, cap=3, [0 0 0]
s2 := make([]int, 0, 5)   // len=0, cap=5, []

make vs new

new allocates memory and returns a pointer to a zero value. make initializes slices, maps, and channels with their internal structure ready to use. You can't use new for slices or maps because they need that internal setup to work.

append

append adds elements and returns a new slice. You must reassign the result.

s := []int{1, 2}
s = append(s, 3)
s = append(s, 4, 5)
fmt.Println(s) // [1 2 3 4 5]

Forgetting to reassign is a common bug. append may allocate a new underlying array.

len vs cap

len is how many elements the slice holds. cap is how many it can hold before needing a new allocation.

s := make([]int, 2, 5)
fmt.Println(len(s)) // 2
fmt.Println(cap(s)) // 5

Slicing

Slicing creates a new slice that shares the same underlying array.

s := []int{10, 20, 30, 40, 50}
fmt.Println(s[1:3]) // [20 30]
fmt.Println(s[:2])  // [10 20]
fmt.Println(s[2:])  // [30 40 50]

Changes to a sub-slice affect the original.

sub := s[1:3]
sub[0] = 99
fmt.Println(s) // [10 99 30 40 50]

copy

copy creates an independent copy. Changes to one don't affect the other.

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 99
fmt.Println(src) // [1 2 3]
fmt.Println(dst) // [99 2 3]

Since Go 1.21, slices.Clone does the same thing in one call:

import "slices"

src := []int{1, 2, 3}
dst := slices.Clone(src)

slices.Clone is just append([]int(nil), src...) internally. Same result, less boilerplate.

nil Slice vs Empty Slice

A nil slice has no underlying array. An empty slice points to an array of length zero. Both have len 0 and work with append.

var nilSlice []int          // nil
emptySlice := []int{}       // not nil, but empty

fmt.Println(nilSlice == nil)   // true
fmt.Println(emptySlice == nil) // false
fmt.Println(len(nilSlice))     // 0
fmt.Println(len(emptySlice))   // 0

Appending to a nil slice doesn't panic. Go allocates the array for you:

var s []int
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]

This means var s []int is a perfectly valid way to start building a slice. No need to make it first.

Maps

A map is an unordered collection of key-value pairs.

m := map[string]int{
    "alice": 90,
    "bob":   85,
}
fmt.Println(m["alice"]) // 90

Like slices, a map is already a pointer internally. When you pass a map to a function, both the caller and the function see the same data. You almost never need *map[K]V.

func addScore(m map[string]int) {
    m["charlie"] = 95 // modifies the caller's map
}

scores := map[string]int{"alice": 90}
addScore(scores)
fmt.Println(scores) // map[alice:90 charlie:95]

The only rare case for *map is when a function needs to replace the entire map, not just modify its contents. But in practice, people just return the new map from the function instead.

make for Maps

Use make to create an empty map.

m := make(map[string]int)
m["score"] = 100
fmt.Println(m) // map[score:100]

You can pass a size hint to avoid rehashing when you know roughly how many keys to expect. It's not a hard limit, the map still grows if needed.

m := make(map[string]int, 100) // preallocate space for ~100 keys

Access, Insert, Delete

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

// Access
fmt.Println(m["a"]) // 1

// Insert or update
m["c"] = 3

// Delete
delete(m, "b")
fmt.Println(m) // map[a:1 c:3]

Accessing a missing key returns the zero value. No error, no panic.

fmt.Println(m["missing"]) // 0

To remove all keys at once, use clear (Go 1.21+). The map stays usable, just empty.

m := map[string]int{"a": 1, "b": 2}
clear(m)
fmt.Println(m)      // map[]
fmt.Println(len(m)) // 0
m["c"] = 3          // still works

Comma-ok Pattern

Use the two-value form to check if a key actually exists.

m := map[string]int{"a": 1}

val, ok := m["a"]
fmt.Println(val, ok) // 1 true

val, ok = m["z"]
fmt.Println(val, ok) // 0 false

if v, ok := m["a"]; ok {
    fmt.Println("found:", v) // found: 1
}

Iteration Order Is Random

Go randomizes map iteration order on purpose. Never rely on it.

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v) // order varies each run
}

Key Takeaways

  • Arrays are fixed-size and rarely used directly. Prefer slices
  • A slice is a header (pointer, len, cap) that points to an underlying array. Go manages the array
  • Passing a slice copies the header, not the data. Both point to the same array
  • append may allocate a new array, so always reassign: s = append(s, val)
  • If a function appends to a caller's slice, pass *[]T. The caller's header won't update otherwise
  • len is current size, cap is allocated capacity
  • Sub-slices share memory with the original. Use copy for independence
  • A nil slice and an empty slice both have len 0 but differ in nil checks
  • Maps are unordered key-value stores created with literals or make
  • Use the comma-ok pattern (val, ok := m[key]) to check key existence
  • Map iteration order is intentionally randomized

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy