Skip to content
ioob.dev
Go back

Go Basics Part 5 — Arrays, Slices, and Maps

· 4 min read
Go Series (5/12)
  1. Go Basics Part 1 — Variables, Constants, and Types
  2. Go Basics Part 2 — Conditionals and Loops
  3. Go Basics Part 3 — Functions
  4. Go Basics Part 4 — Error Handling
  5. Go Basics Part 5 — Arrays, Slices, and Maps
  6. Go Basics Part 6 — Structs and Methods
  7. Go Part 7 — Interfaces
  8. Go Part 8 — Pointers
  9. Go Part 9 — Goroutines and Channels
  10. Go Part 10 — Concurrency Patterns
  11. Go Part 11 — Packages and Modules
  12. Go Part 12 — Generics and Practical Patterns
Table of contents

Table of contents

Arrays — Fixed Size

In Go, an array’s size is part of its type. [3]int and [5]int are completely different types.

package main

import "fmt"

func main() {
    var a [3]int              // [0, 0, 0]
    b := [3]string{"Go", "is", "fun"}

    a[0] = 10
    fmt.Println(a)  // [10 0 0]
    fmt.Println(b)  // [Go is fun]
    fmt.Println(len(b))  // 3
}

The size must be known at compile time and can’t be changed once set. Honestly, directly using arrays in Go is rare. Most of the time you use slices.

So why do arrays exist? Because they form the foundation of slices. To understand slices, you need to know that arrays sit underneath.

Slices — Go’s Real List

Slices are the most commonly used collection in Go. Think of them as a variable-length view on top of an array.

package main

import "fmt"

func main() {
    // Slice literal
    fruits := []string{"apple", "banana", "cherry"}
    fmt.Println(fruits)       // [apple banana cherry]
    fmt.Println(len(fruits))  // 3

    // Create with make
    nums := make([]int, 3, 5)  // length 3, capacity 5
    fmt.Println(nums)          // [0 0 0]
    fmt.Println(len(nums))     // 3
    fmt.Println(cap(nums))     // 5
}

Arrays have a size like [3]string, while slices don’t like []string. This difference is key.

len and cap — The Internal Structure of Slices

To understand how slices work, you need to know their internal structure. A slice is a small struct with three pieces of information:

Looking at the diagram, the concept of a slice as a “view” over an array becomes clear. Let’s visualize a case where two slices look at the same array.

flowchart LR
    subgraph Back[Array storage]
        A0["[0]=1"] --> A1["[1]=2"] --> A2["[2]=3"] --> A3["[3]=4"] --> A4["[4]=5"]
    end
    subgraph Orig[original]
        P1[ptr -> A0]
        L1[len=5]
        C1[cap=5]
    end
    subgraph Sub[sub = original 1:3]
        P2[ptr -> A1]
        L2[len=2]
        C2[cap=4]
    end
    P1 -.reference.- A0
    P2 -.reference.- A1
package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}

    // Slicing — shares the same underlying array
    sub := original[1:3]
    fmt.Println(sub)       // [2 3]
    fmt.Println(len(sub))  // 2
    fmt.Println(cap(sub))  // 4 — capacity from original[1] to the end

    // Changing sub also changes original!
    sub[0] = 99
    fmt.Println(original)  // [1 99 3 4 5]
}

This is the most important thing to watch out for when working with slices. A slice created by slicing shares the underlying array with the original. Changing one side changes the other. It’s a similar trap to Java’s List.subList().

If you need an independent copy, use copy.

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    clone := make([]int, len(original))
    copy(clone, original)

    clone[0] = 99
    fmt.Println(original)  // [1 2 3 4 5] — original unchanged
    fmt.Println(clone)     // [99 2 3 4 5]
}

append — Adding Elements to a Slice

append adds elements to the end of a slice and returns a new slice.

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))  // [] 0 0

    s = append(s, 1)
    fmt.Println(s, len(s), cap(s))  // [1] 1 1

    s = append(s, 2, 3)
    fmt.Println(s, len(s), cap(s))  // [1 2 3] 3 4

    s = append(s, 4, 5, 6)
    fmt.Println(s, len(s), cap(s))  // [1 2 3 4 5 6] 6 8
}

When capacity runs out, the Go runtime allocates a larger array and copies the existing data. You can see the capacity roughly doubling. (The exact growth strategy varies by Go version.)

You must capture the return value. append doesn’t modify the original — it returns a new slice.

// Don't do this
append(s, 1)     // Discarding the return value is meaningless

// Do this
s = append(s, 1)

To concatenate slices, use ....

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := []int{4, 5, 6}
    a = append(a, b...)
    fmt.Println(a)  // [1 2 3 4 5 6]
}

nil Slices and Empty Slices

In Go, a nil slice and an empty slice are different. But in practice, they behave almost identically.

package main

import "fmt"

func main() {
    var nilSlice []int           // nil slice
    emptySlice := []int{}        // empty slice
    madeSlice := make([]int, 0)  // empty slice

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

    // But len, cap, and append all work the same
    fmt.Println(len(nilSlice), len(emptySlice))  // 0 0
    nilSlice = append(nilSlice, 1)
    emptySlice = append(emptySlice, 1)
    fmt.Println(nilSlice, emptySlice)  // [1] [1]
}

len, cap, and append all work normally on a nil slice. So you can declare with var s []int and start appending right away without initialization. This is where Go’s zero value philosophy shines.

However, there’s a difference when serializing to JSON. A nil slice becomes null, while an empty slice becomes []. You need to be aware of this difference when building API responses.

Maps — Key-Value Store

A map is a data structure that associates keys with values. It serves the same role as Python’s dictionary or Java’s HashMap.

package main

import "fmt"

func main() {
    // Create with literal
    scores := map[string]int{
        "Alice": 90,
        "Bob":   85,
        "Carol": 92,
    }
    fmt.Println(scores)           // map[Alice:90 Bob:85 Carol:92]
    fmt.Println(scores["Alice"])  // 90

    // Create with make
    ages := make(map[string]int)
    ages["Tom"] = 25
    ages["Jane"] = 30
    fmt.Println(ages)  // map[Jane:30 Tom:25]
}

When you look up a key that doesn’t exist in a map, the zero value is returned. To check if a key actually exists, use the second return value.

package main

import "fmt"

func main() {
    scores := map[string]int{
        "Alice": 90,
    }

    score := scores["Bob"]
    fmt.Println(score)  // 0 — missing key, but no error

    // Check if key exists
    score, ok := scores["Bob"]
    if !ok {
        fmt.Println("Bob's score not found")
    }

    // This pattern is commonly used
    if score, ok := scores["Alice"]; ok {
        fmt.Println("Alice:", score)
    }
}

value, ok := m[key] — this is called the “comma ok” pattern. It appears throughout Go, so get used to seeing it.

Map Iteration and Deletion

Use range to iterate over maps. One thing to know: iteration order is not guaranteed.

package main

import "fmt"

func main() {
    colors := map[string]string{
        "red":   "#FF0000",
        "green": "#00FF00",
        "blue":  "#0000FF",
    }

    // Iteration — order may vary between runs
    for key, value := range colors {
        fmt.Printf("%s: %s\n", key, value)
    }

    // Delete
    delete(colors, "red")
    fmt.Println(colors)  // map[blue:#0000FF green:#00FF00]

    // Clear all (Go 1.21+)
    clear(colors)
    fmt.Println(colors)  // map[]
}

Go intentionally randomizes map iteration order. This is a design decision to prevent code from depending on order. If you need sorted order, you must collect the keys into a slice, sort it, and then iterate.

delete removes a key, and starting with Go 1.21, clear removes all entries at once. Deleting a nonexistent key doesn’t cause an error, so feel free to use it without checks.


The next part covers Go’s structs and methods. We’ll explore pointer receivers, embedding, constructor patterns, and how Go approaches object-oriented programming.

-> Part 6: Structs and Methods


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Go Basics Part 4 — Error Handling
Next Post
Go Basics Part 6 — Structs and Methods