Lesson 7/41 · Go Language Intro & Dev Environment Beginner m01-l07

Arrays, Slices and Maps: The Three Essential Containers

Module 1: Go Language Intro & Dev Environment | Lesson: m01-l07 | Level: Beginner After this lesson you will be able to: master the core use of arrays, slices and maps, understand slice growth and underlying-array sharing, and avoid common pitfalls such as concurrent map reads and writes.

Learning Objectives

  • Distinguish the memory models of arrays and slices, and understand the structure of a slice header.
  • Use append, copy and slice expressions correctly, and explain slice growth behavior.
  • Perform map inserts, deletes, updates and lookups skillfully, and understand iteration randomness and its effect on deletions.
  • Recognize and avoid the shared-underlying-array trap when passing slices, and the fatal error caused by concurrent map access.

Core Concepts

7.1 Arrays: Fixed-Size Value Types

An array is the most basic data structure in Go: a contiguous block of memory made up of elements of the same type.

  1. What: the length is part of the array type. [5]int and [10]int are two completely different types. An array must have its length specified at declaration, and that length cannot change.
  2. Why: Go chooses fixed-length arrays so that the memory size is known at compile time, yielding very high access efficiency and avoiding the overhead of dynamic allocation.
  3. How: go var a [5]int // declared and initialized to the zero value [0 0 0 0 0] b := [3]int{1, 2, 3} c := [...]int{1, 2, 3} // the compiler infers the length as 3 Note: an array is a value type. When an array is passed as a function argument, its entire contents are copied. On a 64-bit platform, passing an array of length N costs $N \times \text{sizeof(Element)}$ memory.

7.2 Slices: Dynamic Arrays with Reference Semantics

A slice is an abstraction over an array underneath, providing a dynamically sized view.

  1. What: a slice is itself a data structure (a Slice Header) containing three fields:

    • Pointer: a pointer to the slice's starting element in the underlying array.
    • Len: the number of elements in the slice.
    • Cap: the capacity of the underlying array, i.e. the number of elements from the start position to the end of the array.

    A slice is not a reference type; it is a value type (the header — 24 bytes: pointer 8 + len 8 + cap 8 — is copied when passed), but it shares the same underlying array with its sibling slices.

  2. Why: to combine the performance of static arrays with the flexibility of dynamic arrays. By sharing the underlying array, slice operations (such as reslicing) complete in $O(1)$ time without copying data.

  3. How:

    • Creation: go s := make([]int, 3, 5) // len=3, cap=5, underlying array length 5 s2 := []int{1, 2, 3} // len=3, cap=3
    • Slice expression: s[low:high:max]
    • s[low:high]: takes elements from index low to high-1.
    • s[low:high:max]: besides taking elements, it caps the new slice's maximum capacity at max - low. This is commonly used to prevent data leaks or to reuse memory.
    • Iteration: use for range.

7.3 append and the Growth Mechanism

append is the most common slice operation, used to add elements to the end of a slice.

  1. Growth threshold and strategy (Go 1.18+): When the slice runs out of capacity, append allocates a new underlying array and copies the data. The growth strategy is:

    • When oldcap < 256, the new capacity is roughly 2x the old capacity (about doubling).
    • When oldcap >= 256, the new capacity grows by the formula newcap += (newcap + 3*256)/4 (roughly 1.25x), then rounds up to a memory size class.
    • Engineering practice: do not rely on the exact growth factor. If you know the approximate size of a slice, preallocate capacity with make([]T, 0, n) to avoid the allocation and copying cost of repeated growth.
  2. The shared-underlying-array trap: If the slice has enough capacity, append does not trigger growth and writes directly into the underlying array. If multiple slices share the same underlying array, a modification through one slice affects the others.

7.4 map: A Hash Table

A map is Go's built-in hash table implementation, used for key-value storage.

  1. What: map[K]V, where K must be a type supporting == and != (such as int, string, struct, etc.; slices, maps and functions are not allowed).
  2. Iteration is unordered: when for range iterates over a map, the order of key-value pairs is random. It may differ on every run.
  3. Deletion:
    • delete(map, key): removes the given key.
    • Spec semantics: deleting elements during a for range loop is legal. The spec states: if an entry not yet reached is deleted during iteration, it will not be produced.
    • New entries: whether an entry inserted during iteration is produced is undefined (it may or may not show up), so do not add entries during iteration.
    • Concurrency safety: a native map is not concurrency-safe. Concurrent reads and writes on the same map trigger fatal error: concurrent map reads and map writes or concurrent map writes — an unrecoverable fatal error.

Example Code

Example 1: Slice Growth and the Shared-Underlying-Array Trap

This example shows the underlying array being detached when append triggers growth, and the side effects of sharing the underlying array when capacity is sufficient.

package main

import "fmt"

func main() {
    // Scenario 1: capacity runs out, growth triggers, and the underlying arrays separate
    fmt.Println("=== Scenario 1: growth detaches the underlying array ===")
    s1 := []int{1, 2, 3}
    fmt.Printf("s1: %v, cap: %d\n", s1, cap(s1))

    // At this point s1 has cap=3, so append needs to grow
    s2 := append(s1, 4)
    fmt.Printf("s1: %v, cap: %d\n", s1, cap(s1))
    fmt.Printf("s2: %v, cap: %d\n", s2, cap(s2))
    fmt.Println("s1 and s2 now have separate underlying arrays and do not affect each other.")
    fmt.Println()

    // Scenario 2: capacity is sufficient, writing directly into the shared array affects the original slice
    fmt.Println("=== Scenario 2: enough capacity, shared underlying array ===")
    // Create a slice with spare capacity
    s3 := make([]int, 2, 5) // len=2, cap=5
    s3[0] = 10
    s3[1] = 20

    // Create s4 based on s3, pointing at s3's second element
    s4 := s3[1:2] // len=1, cap=4, points at s3[1]

    fmt.Printf("s3: %v\n", s3)
    fmt.Printf("s4: %v\n", s4)

    // Append to s4. Because s4 has enough cap (4 > 1), no growth happens,
    // and it writes directly into s3's underlying array at s3[2].
    s4 = append(s4, 99)

    fmt.Printf("s3: %v\n", s3) // s3 is modified!
    fmt.Printf("s4: %v\n", s4)
    fmt.Println("Note: s3[2] was modified by s4's append, because they share the underlying array.")
}

Expected output:

=== Scenario 1: growth detaches the underlying array ===
s1: [1 2 3], cap: 3
s1: [1 2 3], cap: 3
s2: [1 2 3 4], cap: 6
s1 and s2 now have separate underlying arrays and do not affect each other.

=== Scenario 2: enough capacity, shared underlying array ===
s3: [10 20 0]
s4: [20]
s3: [10 20 99]
s4: [20 99]
Note: s3[2] was modified by s4's append, because they share the underlying array.

Example 2: Map Iteration and Deletion

This example shows map insert/update/lookup/delete, and the semantics of safely deleting elements during iteration.

package main

import "fmt"

func main() {
    // 1. Create and initialize
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }

    // 2. Insert, update, lookup
    m["d"] = 4 // insert
    m["a"] = 10 // update
    val, ok := m["b"] // lookup; ok is true
    fmt.Printf("m['b']: %v, ok: %v\n", val, ok)

    // 3. Check whether a key exists
    if _, exists := m["e"]; !exists {
        fmt.Println("Key 'e' does not exist")
    }

    // 4. Iteration and deletion
    fmt.Println("Before delete:")
    for k, v := range m {
        fmt.Printf("%s: %d\n", k, v)
    }

    // Deleting during iteration is legal.
    // Spec: entries not yet reached that are deleted are no longer produced.
    fmt.Println("\nDeleting 'a' during iteration:")
    for k, v := range m {
        if k == "a" {
            delete(m, k)
            fmt.Printf("Deleted %s\n", k)
        }
    }

    fmt.Println("\nAfter delete:")
    for k, v := range m {
        fmt.Printf("%s: %d\n", k, v)
    }
}

Expected output:

m['b']: 2, ok: true
Key 'e' does not exist
Before delete:
a: 10
b: 2
c: 3
d: 4

Deleting 'a' during iteration:
Deleted a

After delete:
b: 2
c: 3
d: 4

(Note: map iteration order is random; the output above is just one possibility. It only needs to contain b, c, d and no a.)

Common Mistakes & Pitfalls

  1. Wrong assumption: a slice is a reference type and passing it does not copy

    • Why: the slice header (pointer, length, capacity) is a value type, so passing a slice copies the pointer, len and cap (24 bytes total). Although the header is copied, the pointer points at the same underlying array, so modifying elements affects the original slice.
    • Correct approach: understand that a slice is "a value type that shares its underlying array". If you need an independent copy, use copy or append([]T(nil), s...).
  2. Wrong assumption: deleting while iterating a map panics

    • Why: the Go spec explicitly allows deleting unreached keys during for range. What actually causes a fatal error is concurrent access (goroutine A reads while goroutine B writes).
    • Correct approach: deleting during iteration is legal. If you want to avoid deleting unreached entries, collect the keys to delete first and bulk-delete after the loop.
  3. Wrong assumption: append(s, x)[:len(s)] preserves the original slice

    • Why: if the original slice has spare capacity, append does not allocate a new array but modifies the underlying array directly. Then s and t share the underlying array, and modifying t affects s.
    • Correct approach: if you need a fully independent slice, use a three-index slice append(s[:len(s):len(s)], x) or copy.

Best Practices

  1. Preallocate capacity: when creating a slice or map with make, provide a capacity argument whenever possible (e.g. make([]int, 0, 100)) to reduce the number of growths triggered by append.
  2. Avoid the shared-underlying-array trap: when passing a slice to a function that might modify it, and you do not want the original data affected, copy the slice first (copy or a new append slice).
  3. Map concurrency safety: when reading and writing a map concurrently, you must use sync.Mutex or sync.Map. Do not try to recover from a concurrent-map-write panic; it is a fatal error.
  4. Cap capacity when reslicing: use s[low:high:high] or s[low:high:max] to bound the new slice's cap, preventing an oversized underlying array from causing a memory leak.

Exercises

  1. Concept question: Explain why var a [5]int and var b [10]int are different types, while []int and []int are the same type.

  2. Coding exercise: Write a function RemoveDuplicates that takes a []int slice and returns a deduplicated []int slice. Requirements:

    • Each element appears exactly once in the result.
    • Preserve the order of each element's first appearance in the original slice.
    • Do not use any external libraries.
  3. Advanced/debugging exercise: The code below is meant to find duplicate elements in a slice, but has a logic error. Find the problem and fix it, so that each duplicated element appears exactly once in the result, in the order of its first repeat.

    ```go package main

    import "fmt"

    func FindDuplicates(nums []int) []int { seen := make(map[int]bool) var duplicates []int for _, num := range nums { if seen[num] { duplicates = append(duplicates, num) } else { seen[num] = true } } return duplicates }

    func main() { nums := []int{1, 2, 3, 2, 4, 1, 5} fmt.Println(FindDuplicates(nums)) } `` *(Note: the expected output is[2 1]`, because 2 and 1 are duplicated, and 2 is encountered a second time before 1 is.)*

Reference Answers

Reference Answers (click to expand / collapse)

Question 1

The array length is part of the type signature; the compiler fixes the array size at compile time, so [5]int and [10]int have different memory layouts and are treated as different types. A slice, on the other hand, is a dynamic view whose type only cares about the element type int, not the length, so []int and []int are the same type.

Question 2

package main

import "fmt"

func RemoveDuplicates(nums []int) []int {
    seen := make(map[int]bool)
    var result []int
    for _, num := range nums {
        if !seen[num] {
            seen[num] = true
            result = append(result, num)
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 2, 4, 1, 5}
    fmt.Println(RemoveDuplicates(nums)) // prints: [1 2 3 4 5]
}

Question 3

Problem analysis: the original code appends a duplicated element directly to the duplicates slice every time it sees one. If the same element appears multiple times (e.g. [1, 1, 1]), it gets appended to the result multiple times. The task requires "each duplicated element appears exactly once".

Fixed code:

package main

import "fmt"

func FindDuplicates(nums []int) []int {
    seen := make(map[int]bool)
    duplicates := make(map[int]bool)
    var result []int

    for _, num := range nums {
        if seen[num] {
            // If already marked as duplicate, skip it
            if !duplicates[num] {
                duplicates[num] = true
                result = append(result, num)
            }
        } else {
            seen[num] = true
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 2, 4, 1, 5}
    fmt.Println(FindDuplicates(nums)) // prints: [2 1]
}

Lesson Summary

  • Arrays are fixed-length value types; slices are value types that share an underlying array (the header holds a pointer, len and cap).
  • append triggers growth when capacity runs out; the Go 1.18+ strategy is roughly 2x for oldcap < 256 and ~1.25x rounded up for >= 256.
  • Passing a slice copies the header but shares the underlying array, so watch out for append modifying the original data.
  • Map iteration is unordered; deleting unreached keys during for range is legal, but concurrent map access causes a fatal error.
  • In practice, preallocate slice and map capacity to avoid frequent growth.

Next lesson preview: strings and Unicode — rune, byte and the strings package.

Feedback

This site is front-end only — no backend, no accounts — so feedback goes to GitHub Issues, Discussions, or email.

Paste it into the issue or email to help track it down — it never includes anything you typed

Also:Browse existing issues · Email feedback (no GitHub account needed):278975598@qq.com