Cristhian Villegas
Backend10 min read2 views

Arrays, Slices and Maps in Go: Fundamental Data Structures

Arrays, Slices and Maps in Go: Fundamental Data Structures

Go Logo

Introduction: data collections in Go

Welcome to part 5 of our Go course for beginners. So far we have worked with individual variables, but in real-world programming we almost always need to handle groups of data. Go provides three fundamental structures for organizing collections: arrays, slices and maps.

Unlike languages such as Python or JavaScript, Go makes a clear distinction between arrays (fixed size) and slices (dynamic size). Understanding this difference is key to writing efficient Go code. In this article we will learn each structure with practical, runnable examples.

Arrays: fixed-size collections

An array in Go is a collection of elements of the same type with a fixed size defined at declaration time. Once created, you cannot change its size.

Declaration and basic usage

go
1package main
2
3import "fmt"
4
5func main() {
6    // Declare an array of 5 integers (initialized to 0)
7    var numbers [5]int
8    fmt.Println(numbers) // [0 0 0 0 0]
9
10    // Assign values by index
11    numbers[0] = 10
12    numbers[1] = 20
13    numbers[4] = 50
14    fmt.Println(numbers) // [10 20 0 0 50]
15
16    // Declare and initialize at the same time
17    fruits := [3]string{"apple", "banana", "orange"}
18    fmt.Println(fruits) // [apple banana orange]
19
20    // Let Go count the elements with [...]
21    colors := [...]string{"red", "green", "blue", "yellow"}
22    fmt.Println(colors)      // [red green blue yellow]
23    fmt.Println(len(colors)) // 4
24}
Warning: In Go, the array size is part of its type. This means [3]int and [5]int are different types and you cannot assign one to the other. For this reason, slices are used much more often in practice.

Accessing elements and iterating

go
1package main
2
3import "fmt"
4
5func main() {
6    grades := [4]float64{9.5, 8.0, 7.5, 10.0}
7
8    // Access by index (starts at 0)
9    fmt.Println("First grade:", grades[0]) // 9.5
10    fmt.Println("Last grade:", grades[3])  // 10.0
11
12    // Iterate with classic for
13    for i := 0; i < len(grades); i++ {
14        fmt.Printf("Grade %d: %.1f\n", i+1, grades[i])
15    }
16
17    // Iterate with range (idiomatic way)
18    total := 0.0
19    for _, grade := range grades {
20        total += grade
21    }
22    average := total / float64(len(grades))
23    fmt.Printf("Average: %.2f\n", average) // 8.75
24}

Slices: dynamic arrays

A slice is a reference to a section of an underlying array. Unlike arrays, slices have a dynamic size and are the structure you will use most in Go for ordered collections.

Creating slices

go
1package main
2
3import "fmt"
4
5func main() {
6    // Method 1: slice literal (no size)
7    fruits := []string{"apple", "banana", "orange"}
8    fmt.Println(fruits) // [apple banana orange]
9
10    // Method 2: with make(type, length, capacity)
11    numbers := make([]int, 3, 10)
12    fmt.Println(numbers)      // [0 0 0]
13    fmt.Println(len(numbers)) // 3 (current length)
14    fmt.Println(cap(numbers)) // 10 (maximum capacity before reallocation)
15
16    // Method 3: empty slice
17    var empty []int
18    fmt.Println(empty)        // []
19    fmt.Println(empty == nil) // true (a nil slice is valid)
20    fmt.Println(len(empty))   // 0
21}
Note: The make() function is the recommended way when you know the approximate size of your slice. This avoids unnecessary memory reallocations and improves performance.

Slice internals: length and capacity

To truly understand slices, you need to know their three internal components: a pointer to the underlying array, the length (len) and the capacity (cap). Length is the number of elements it contains, and capacity is the available space before needing to reallocate memory.

go
1package main
2
3import "fmt"
4
5func main() {
6    // Create a slice with make
7    s := make([]int, 3, 5)
8    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
9    // len=3 cap=5 [0 0 0]
10
11    // Add elements with append
12    s = append(s, 10)
13    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
14    // len=4 cap=5 [0 0 0 10]
15
16    s = append(s, 20)
17    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
18    // len=5 cap=5 [0 0 0 10 20]
19
20    // When exceeding capacity, Go creates a larger underlying array
21    s = append(s, 30)
22    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
23    // len=6 cap=10 [0 0 0 10 20 30]
24    // Capacity doubled automatically
25}
Tip: When Go needs more space, it doubles the slice capacity (for small slices). So if you know how many elements you will have, use make() with the correct capacity to avoid unnecessary copies.

Slice operations

Go offers several operations to manipulate slices. Let us look at the most important ones: slicing, copying and deleting elements.

Slicing

go
1package main
2
3import "fmt"
4
5func main() {
6    numbers := []int{10, 20, 30, 40, 50, 60, 70}
7
8    // slice[start:end] - from start to end-1
9    fmt.Println(numbers[1:4]) // [20 30 40]
10    fmt.Println(numbers[:3])  // [10 20 30] (from the beginning)
11    fmt.Println(numbers[4:])  // [50 60 70] (to the end)
12    fmt.Println(numbers[:])   // [10 20 30 40 50 60 70] (full copy)
13}

Copying slices

go
1package main
2
3import "fmt"
4
5func main() {
6    original := []int{1, 2, 3, 4, 5}
7
8    // WARNING: this does NOT copy, both point to the same array
9    reference := original
10    reference[0] = 99
11    fmt.Println(original) // [99 2 3 4 5] (modified!)
12
13    // Correct way: use copy()
14    original = []int{1, 2, 3, 4, 5}
15    clone := make([]int, len(original))
16    copy(clone, original)
17    clone[0] = 99
18    fmt.Println(original) // [1 2 3 4 5] (not modified)
19    fmt.Println(clone)    // [99 2 3 4 5]
20}

Deleting elements

go
1package main
2
3import "fmt"
4
5func main() {
6    names := []string{"Ana", "Luis", "Maria", "Carlos", "Pedro"}
7
8    // Delete element at index 2 ("Maria")
9    index := 2
10    names = append(names[:index], names[index+1:]...)
11    fmt.Println(names) // [Ana Luis Carlos Pedro]
12
13    // Append multiple elements
14    names = append(names, "Sofia", "Diego")
15    fmt.Println(names) // [Ana Luis Carlos Pedro Sofia Diego]
16}
Warning: The delete technique append(s[:i], s[i+1:]...) preserves the original order but can be slow for very large slices since it shifts all subsequent elements. For unordered slices, you can swap the target with the last element and truncate.

Maps: key-value dictionaries

A map is a collection of key-value pairs, similar to a dictionary in Python or an object in JavaScript. Keys must be a comparable type (string, int, etc.) and values can be any type.

Declaration and CRUD operations

go
1package main
2
3import "fmt"
4
5func main() {
6    // Create a map with make
7    ages := make(map[string]int)
8
9    // CREATE: add elements
10    ages["Ana"] = 25
11    ages["Luis"] = 30
12    ages["Maria"] = 28
13    fmt.Println(ages) // map[Ana:25 Luis:30 Maria:28]
14
15    // READ: get a value
16    fmt.Println("Ana's age:", ages["Ana"]) // 25
17
18    // UPDATE: change a value
19    ages["Ana"] = 26
20    fmt.Println("Ana's new age:", ages["Ana"]) // 26
21
22    // DELETE: remove an element
23    delete(ages, "Luis")
24    fmt.Println(ages) // map[Ana:26 Maria:28]
25
26    // Map literal (declare and initialize)
27    capitals := map[string]string{
28        "Mexico":    "Mexico City",
29        "Argentina": "Buenos Aires",
30        "Colombia":  "Bogota",
31    }
32    fmt.Println(capitals)
33}

The comma-ok idiom

When you read a value from a map with a key that does not exist, Go returns the zero value for that type (0 for int, "" for string). To know whether a key actually exists, use the comma-ok pattern:

go
1package main
2
3import "fmt"
4
5func main() {
6    ages := map[string]int{
7        "Ana":   25,
8        "Maria": 28,
9    }
10
11    // Without comma-ok: you cannot tell if 0 is real or missing
12    fmt.Println(ages["Pedro"]) // 0 (does not exist, but looks like a value)
13
14    // With comma-ok: check if the key exists
15    age, exists := ages["Pedro"]
16    if exists {
17        fmt.Println("Pedro's age:", age)
18    } else {
19        fmt.Println("Pedro is not in the map")
20    }
21
22    // Compact form (idiomatic Go)
23    if age, ok := ages["Ana"]; ok {
24        fmt.Println("Ana's age:", age) // 25
25    }
26}
Tip: Always use the value, ok := myMap[key] pattern when you need to check if a key exists. It is one of the most important conventions in Go and prevents subtle bugs.

Iterating with range

The range keyword is the idiomatic way to iterate over arrays, slices and maps in Go. It is similar to for...of in JavaScript or for...in in Python.

go
1package main
2
3import "fmt"
4
5func main() {
6    // Range over a slice (index, value)
7    fruits := []string{"apple", "banana", "orange"}
8    for i, fruit := range fruits {
9        fmt.Printf("%d: %s\n", i, fruit)
10    }
11
12    // If you do not need the index, use _
13    for _, fruit := range fruits {
14        fmt.Println(fruit)
15    }
16
17    // If you only need the index
18    for i := range fruits {
19        fmt.Printf("Index: %d\n", i)
20    }
21
22    // Range over a map (key, value)
23    ages := map[string]int{"Ana": 25, "Luis": 30, "Maria": 28}
24    for name, age := range ages {
25        fmt.Printf("%s is %d years old\n", name, age)
26    }
27}
Warning: The iteration order of a map is not guaranteed in Go. Each time you iterate, the order may differ. If you need a specific order, sort the keys first into a slice.

Nested structures

In real applications, it is common to combine slices and maps to represent complex data. Let us see how to create a slice of maps and a map of slices.

go
1package main
2
3import "fmt"
4
5func main() {
6    // Slice of maps: list of students
7    students := []map[string]string{
8        {"name": "Ana", "major": "Engineering"},
9        {"name": "Luis", "major": "Medicine"},
10        {"name": "Maria", "major": "Law"},
11    }
12
13    for _, s := range students {
14        fmt.Printf("%s studies %s\n", s["name"], s["major"])
15    }
16
17    // Map of slices: courses by department
18    courses := map[string][]string{
19        "Engineering": {"Calculus", "Physics", "Programming"},
20        "Medicine":    {"Anatomy", "Physiology", "Biochemistry"},
21        "Law":         {"Civil", "Criminal", "Constitutional"},
22    }
23
24    fmt.Println("\nEngineering courses:")
25    for _, course := range courses["Engineering"] {
26        fmt.Println("  -", course)
27    }
28
29    // Add a new course
30    courses["Engineering"] = append(courses["Engineering"], "Statistics")
31    fmt.Println("\nUpdated courses:", courses["Engineering"])
32}

Practical examples: word counter and grade tracker

Let us build two practical programs that combine everything we have learned.

Word counter

go
1package main
2
3import (
4    "fmt"
5    "strings"
6)
7
8func countWords(text string) map[string]int {
9    counter := make(map[string]int)
10    words := strings.Fields(strings.ToLower(text))
11    for _, word := range words {
12        counter[word]++
13    }
14    return counter
15}
16
17func main() {
18    text := "go is great go is fast and go is simple"
19    result := countWords(text)
20
21    fmt.Println("Word frequency:")
22    for word, count := range result {
23        fmt.Printf("  %-10s: %d\n", word, count)
24    }
25}

Student grade tracker

go
1package main
2
3import "fmt"
4
5func main() {
6    // Map of slices: grades per student
7    grades := map[string][]float64{
8        "Ana":   {9.5, 8.0, 9.0, 10.0},
9        "Luis":  {7.0, 6.5, 8.0, 7.5},
10        "Maria": {10.0, 9.5, 9.0, 9.8},
11    }
12
13    // Add a new grade for Ana
14    grades["Ana"] = append(grades["Ana"], 8.5)
15
16    // Calculate averages
17    fmt.Println("=== Grade Report ===")
18    for name, scores := range grades {
19        total := 0.0
20        for _, score := range scores {
21            total += score
22        }
23        average := total / float64(len(scores))
24
25        status := "Pass"
26        if average < 7.0 {
27            status = "Fail"
28        }
29        fmt.Printf("%-8s | Average: %.2f | %s\n", name, average, status)
30    }
31
32    // Find the top student
33    topName := ""
34    topAvg := 0.0
35    for name, scores := range grades {
36        total := 0.0
37        for _, score := range scores {
38            total += score
39        }
40        avg := total / float64(len(scores))
41        if avg > topAvg {
42            topAvg = avg
43            topName = name
44        }
45    }
46    fmt.Printf("\nTop student: %s (%.2f)\n", topName, topAvg)
47}
Next article: In part 6 we will learn about structs, methods and interfaces in Go. We will discover how Go implements object-oriented programming without classes, using composition instead of inheritance.
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles