In the previous tutorial, you learned about control flow with if, switch, and for. Now it is time to learn about Go’s most important data structures: arrays, slices, and maps.
Slices and maps are the collections you will use every day in Go. Arrays exist too, but you will almost always use slices instead.
Arrays
An array in Go has a fixed size. You set the size when you declare it, and it cannot change:
package main
import "fmt"
func main() {
// Declare an array of 5 integers
var numbers [5]int
fmt.Println("Default:", numbers) // [0 0 0 0 0]
// Set values by index
numbers[0] = 10
numbers[1] = 20
numbers[4] = 50
fmt.Println("Updated:", numbers) // [10 20 0 0 50]
// Declare and initialize
fruits := [3]string{"apple", "banana", "cherry"}
fmt.Println("Fruits:", fruits)
// Let the compiler count the elements
colors := [...]string{"red", "green", "blue", "yellow"}
fmt.Println("Colors:", colors)
fmt.Println("Length:", len(colors)) // 4
}
Output:
Default: [0 0 0 0 0]
Updated: [10 20 0 0 50]
Fruits: [apple banana cherry]
Colors: [red green blue yellow]
Length: 4
Arrays are useful when you know the exact size at compile time. But in practice, slices are much more common.
Important: In Go, an array’s size is part of its type. [3]int and [5]int are different types. You cannot assign one to the other.
Slices — The Workhorse of Go
A slice is a dynamic, flexible view of an array. It can grow and shrink. You will use slices far more than arrays:
package main
import "fmt"
func main() {
// Create a slice with a literal
names := []string{"Alex", "Sam", "Jordan"}
fmt.Println("Names:", names)
fmt.Println("Length:", len(names))
// Create an empty slice with make
scores := make([]int, 3) // length 3, all zeros
fmt.Println("Scores:", scores)
// Create with length and capacity
buffer := make([]int, 0, 10) // length 0, capacity 10
fmt.Println("Buffer length:", len(buffer))
fmt.Println("Buffer capacity:", cap(buffer))
}
Output:
Names: [Alex Sam Jordan]
Length: 3
Scores: [0 0 0]
Buffer length: 0
Buffer capacity: 10
append — Adding Elements
Use append to add elements to a slice. It returns a new slice:
package main
import "fmt"
func main() {
var numbers []int // nil slice
fmt.Println("Before:", numbers, "len:", len(numbers))
numbers = append(numbers, 1)
numbers = append(numbers, 2, 3)
numbers = append(numbers, 4, 5, 6)
fmt.Println("After:", numbers, "len:", len(numbers))
// Append one slice to another
more := []int{7, 8, 9}
numbers = append(numbers, more...)
fmt.Println("Combined:", numbers)
}
Output:
Before: [] len: 0
After: [1 2 3 4 5 6] len: 6
Combined: [1 2 3 4 5 6 7 8 9]
Important: Always assign the result of append back to the variable. append may create a new underlying array if the capacity is full.
Slice Internals — Pointer, Length, Capacity
A slice is a small struct with three fields:
- Pointer — points to the underlying array
- Length — how many elements the slice contains
- Capacity — how many elements the underlying array can hold
package main
import "fmt"
func main() {
s := make([]int, 3, 5)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=3 cap=5 [0 0 0]
s = append(s, 1)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=4 cap=5 [0 0 0 1]
s = append(s, 2)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=5 cap=5 [0 0 0 1 2]
// Next append exceeds capacity — Go allocates a new, larger array
s = append(s, 3)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=6 cap=10 [0 0 0 1 2 3]
}
When you append beyond the capacity, Go creates a new array (usually double the size) and copies the elements. This is why you should use make([]T, 0, expectedSize) when you know the approximate size. It avoids extra allocations.
Slicing — Creating Sub-Slices
You can create a new slice from an existing one using the [low:high] syntax:
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Slice from index 2 to 5 (not including 5)
a := numbers[2:5]
fmt.Println("numbers[2:5]:", a) // [2 3 4]
// From the start to index 3
b := numbers[:3]
fmt.Println("numbers[:3]:", b) // [0 1 2]
// From index 7 to the end
c := numbers[7:]
fmt.Println("numbers[7:]:", c) // [7 8 9]
// Copy the entire slice
d := numbers[:]
fmt.Println("numbers[:]:", d) // [0 1 2 3 4 5 6 7 8 9]
}
Warning: A sub-slice shares the same underlying array. If you change an element in the sub-slice, it changes in the original too:
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2 3]
sub[0] = 99
fmt.Println("Sub:", sub) // [99 3]
fmt.Println("Original:", original) // [1 99 3 4 5]
}
To avoid this, use copy or the three-index slice [low:high:max] to limit capacity.
copy — Safe Duplication
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
copied := copy(dst, src)
fmt.Println("Copied:", copied, "elements")
fmt.Println("Source:", src)
fmt.Println("Destination:", dst)
// Modifying dst does NOT affect src
dst[0] = 99
fmt.Println("After change:")
fmt.Println("Source:", src) // [1 2 3 4 5]
fmt.Println("Destination:", dst) // [99 2 3 4 5]
}
Removing Elements
Go does not have a built-in remove function. You use slicing and append:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Remove element at index 2 (value 3)
index := 2
numbers = append(numbers[:index], numbers[index+1:]...)
fmt.Println("After remove:", numbers) // [1 2 4 5]
}
Maps
A map is a collection of key-value pairs. It is similar to a dictionary in Python or a HashMap in Java:
package main
import "fmt"
func main() {
// Create a map with a literal
ages := map[string]int{
"Alex": 25,
"Sam": 30,
"Jordan": 22,
}
fmt.Println("Ages:", ages)
// Create an empty map with make
scores := make(map[string]int)
scores["math"] = 95
scores["science"] = 88
fmt.Println("Scores:", scores)
}
Output:
Ages: map[Alex:25 Jordan:22 Sam:30]
Scores: map[math:95 science:88]
Adding, Updating, and Reading
package main
import "fmt"
func main() {
m := make(map[string]string)
// Add entries
m["name"] = "Alex"
m["city"] = "Berlin"
fmt.Println("Map:", m)
// Update a value
m["city"] = "Munich"
fmt.Println("Updated:", m)
// Read a value
name := m["name"]
fmt.Println("Name:", name)
// Reading a missing key returns the zero value
country := m["country"]
fmt.Println("Country:", country) // "" (empty string)
}
Checking if a Key Exists
Use the two-value form to check if a key exists:
package main
import "fmt"
func main() {
ages := map[string]int{
"Alex": 25,
"Sam": 30,
}
// Check if a key exists
age, ok := ages["Alex"]
if ok {
fmt.Println("Alex's age:", age)
}
// Common pattern — check and use in one line
if age, ok := ages["Jordan"]; ok {
fmt.Println("Jordan's age:", age)
} else {
fmt.Println("Jordan not found")
}
}
Output:
Alex's age: 25
Jordan not found
The ok variable is true if the key exists, false otherwise. This is important because reading a missing key gives you the zero value, which could be a valid value (like 0 for integers).
Deleting from Maps
Use the built-in delete function:
package main
import "fmt"
func main() {
colors := map[string]string{
"r": "red",
"g": "green",
"b": "blue",
}
fmt.Println("Before:", colors)
delete(colors, "g")
fmt.Println("After:", colors)
// Deleting a key that does not exist is safe — no error
delete(colors, "x")
}
Output:
Before: map[b:blue g:green r:red]
After: map[b:blue r:red]
Iterating Over Maps
Use for range to iterate:
package main
import "fmt"
func main() {
population := map[string]int{
"Berlin": 3700000,
"Munich": 1500000,
"Hamburg": 1900000,
}
for city, pop := range population {
fmt.Printf("%s: %d\n", city, pop)
}
}
Important: Map iteration order is random in Go. You will get a different order each time you run the program. If you need a specific order, sort the keys first.
A Complete Example
Here is a program that combines slices and maps:
package main
import (
"fmt"
"sort"
)
func main() {
fmt.Println("=== GO-6: Slices and Maps ===")
fmt.Println()
// Student grades
grades := map[string][]int{
"Alex": {85, 92, 78, 90},
"Sam": {70, 65, 80, 75},
"Jordan": {95, 98, 92, 96},
}
// Calculate averages
type result struct {
name string
average float64
}
var results []result
for name, scores := range grades {
sum := 0
for _, s := range scores {
sum += s
}
avg := float64(sum) / float64(len(scores))
results = append(results, result{name: name, average: avg})
}
// Sort by average (highest first)
sort.Slice(results, func(i, j int) bool {
return results[i].average > results[j].average
})
// Print results
fmt.Println("Student Rankings:")
for rank, r := range results {
fmt.Printf(" %d. %s — %.1f%%\n", rank+1, r.name, r.average)
}
fmt.Println()
// Word frequency counter
words := []string{"go", "is", "great", "go", "is", "simple", "go", "is", "fast"}
frequency := make(map[string]int)
for _, w := range words {
frequency[w]++
}
fmt.Println("Word frequency:")
for word, count := range frequency {
fmt.Printf(" %s: %d\n", word, count)
}
}
Output:
=== GO-6: Slices and Maps ===
Student Rankings:
1. Jordan — 95.2%
2. Alex — 86.2%
3. Sam — 72.5%
Word frequency:
go: 3
is: 3
great: 1
simple: 1
fast: 1
Common Mistakes
1. Forgetting to assign the result of append.
// Wrong — the result is lost
append(numbers, 42)
// Correct
numbers = append(numbers, 42)
append returns a new slice. If you don’t assign it, the new element is lost.
2. Using a nil map.
// Wrong — this panics at runtime
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
// Correct — initialize the map first
m := make(map[string]int)
m["key"] = 1
A nil slice is safe to append to, but a nil map is not safe to write to. Always use make or a map literal.
3. Assuming map order is fixed.
// The order of range over maps is random
for k, v := range myMap {
// Different order each run!
}
If you need sorted output, collect the keys into a slice, sort the slice, then iterate.
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #5: Control Flow — if, switch, for loops
- Go Tutorial #4: Functions and Error Handling — Functions and the error pattern
What’s Next?
In the next tutorial, Go Tutorial #7: Structs, Methods, and Composition, you will learn:
- How to define structs — Go’s way to group related data
- Methods — functions attached to types
- Composition with embedding — Go’s alternative to inheritance
- Constructor functions and struct tags
This is part 6 of the Go Tutorial series. Follow along to learn Go from scratch.