Table of contents
- Arrays — Fixed Size
- Slices — Go’s Real List
- len and cap — The Internal Structure of Slices
- append — Adding Elements to a Slice
- nil Slices and Empty Slices
- Maps — Key-Value Store
- Map Iteration and Deletion
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:
- Pointer: The starting position of the actual data in the underlying array
- len: The number of elements currently stored
- cap: The maximum number of elements that can be stored without allocating a new array
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.




Loading comments...