Arrays, Slices and Maps in Go: Fundamental Data Structures

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
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}
[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
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
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}
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.
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}
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
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
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
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}
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
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:
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}
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.
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}
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.
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
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
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}
Comments
Sign in to leave a comment
No comments yet. Be the first!