Why Data Structures Even Matter
Imagine you're organizing your kitchen. You wouldn't throw every item — spices, plates, knives, cereal — into one giant drawer. You'd use containers that fit the job: a spice rack for spices, a shelf for plates.
Data structures are the containers programmers use to organize information so a program can find, use, and change that information efficiently. Go gives you a small set of these containers, and once you understand them, you understand 90% of everyday Go code.
Here's our roadmap:
Arrays — the rigid box
Slices — the flexible box (the one you'll actually use)
Maps — the labeled drawer
Structs — the custom-built container
Pointers — the "address" that lets containers talk to each other
Putting it together — building a simple Stack
Let's go one at a time, slowly.
1. Arrays — The Rigid Box
An array is a fixed-size container. You decide how many slots it has, and that number can never change.
Think of it like an egg carton that holds exactly 6 eggs. You can swap out an egg for a different one, but you can't magically add a 7th slot. If you need more room, you need an entirely new carton.
go
package main
import "fmt"
func main() {
var eggs [6]string // an array of exactly 6 strings
eggs[0] = "brown egg"
eggs[1] = "white egg"
fmt.Println(eggs)
fmt.Println("How many slots?", len(eggs)) // len = length
}What's happening here, line by line:
var eggs [6]string— "Make a container calledeggs. It has 6 slots. Each slot holds a string (text)."eggs[0] = "brown egg"— "Put 'brown egg' into slot number 0." (Go, like most languages, starts counting at 0, not 1.)len(eggs)— a built-in function that tells you how many slots the container has.
Why arrays feel limiting: In real programs, you rarely know in advance exactly how many items you'll have. That's why Go developers almost never use plain arrays directly — they use slices instead.
2. Slices — The Flexible Box (Your New Best Friend)
If an array is a rigid egg carton, a slice is a stretchy grocery bag. Need to add more eggs? Just add them — the bag stretches.
Slices are, without exaggeration, the most-used data structure in everyday Go code.
go
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana"} // a slice, notice: no number in the brackets
fmt.Println(fruits)
fruits = append(fruits, "mango") // add a new item
fmt.Println(fruits)
fmt.Println("First fruit:", fruits[0])
fmt.Println("How many fruits?", len(fruits))
}Breaking it down:
[]string{...}— the empty[](no number inside) is what makes this a slice, not an array.append(fruits, "mango")— this is how you add something to a slice. Important:appenddoesn't change the bag in place, it hands you back a (possibly new) bag, so you always writefruits = append(fruits, ...).fruits[0]— grabs the item sitting in position 0.
The "secret" behind slices
Here's the part that trips people up, so let's make it dead simple: a slice is actually a small wrapper around an array that Go manages for you.
Picture it like this: you hand your groceries to a personal assistant (the slice). Behind the scenes, the assistant keeps things in some storage room (a hidden array). When you ask for more space, the assistant might quietly move everything to a bigger storage room — but from your side, it just looks like the bag stretched.
Three properties matter here:
Length — how many items are currently in the bag (
len(fruits))Capacity — how much room is currently available before Go needs to grow the hidden storage room (
cap(fruits))
go
fmt.Println("Length:", len(fruits))
fmt.Println("Capacity:", cap(fruits))You don't need to obsess over capacity as a beginner — just know it exists and that Go handles the resizing for you automatically.
Slicing a slice (this is where the name comes from)
go
numbers := []int{10, 20, 30, 40, 50}
piece := numbers[1:3] // "give me from index 1 up to (not including) index 3"
fmt.Println(piece) // [20 30]Think of [1:3] as "start at position 1, stop right before position 3."
3. Maps — The Labeled Drawer
A map is like a filing cabinet where every drawer has a label, and inside each labeled drawer is exactly one item. You don't search through drawers one by one — you go straight to the label you want.
In programming terms: a map stores key → value pairs.
go
package main
import "fmt"
func main() {
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
fmt.Println(ages["Alice"]) // 30
ages["Charlie"] = 40 // add a new labeled drawer
fmt.Println(ages)
delete(ages, "Bob") // remove a drawer entirely
fmt.Println(ages)
}Line by line:
map[string]int— "a filing cabinet where labels are strings (names) and contents are integers (ages)."ages["Alice"]— go straight to the "Alice" drawer and grab what's inside.delete(ages, "Bob")— remove the "Bob" drawer completely.
A common gotcha: checking if a key exists
What if you ask for a drawer labeled "Zach" who was never added? Go won't crash — it just gives you the "zero value" (0 for numbers, empty string for text). That can hide bugs, so Go gives you a way to explicitly check:
go
age, exists := ages["Zach"]
if !exists {
fmt.Println("Zach isn't in the cabinet.")
} else {
fmt.Println("Zach's age:", age)
}This "comma, ok" pattern (value, exists := ...) shows up constantly in Go — get comfortable with it early.
4. Structs — Your Custom-Built Container
Arrays, slices, and maps are great, but sometimes you need to bundle different types of related information together — like a person's name (text), age (number), and whether they're active (true/false).
A struct is a blueprint you design yourself. Think of it like a form with labeled fields you fill in.
go
package main
import "fmt"
type Person struct {
Name string
Age int
Active bool
}
func main() {
p := Person{
Name: "Alice",
Age: 30,
Active: true,
}
fmt.Println(p.Name) // "dot" notation to access a field
fmt.Println(p)
}What's going on:
type Person struct { ... }— "I'm creating a new blueprint calledPerson. It has three fields: Name, Age, Active."p := Person{...}— "Fill out that form and call the filled-out copyp."p.Name— "Look at theNamefield on this particular copy of the form."
Slices of structs (the combo you'll use constantly)
Real Go code is full of "a bunch of these custom containers":
go
people := []Person{
{Name: "Alice", Age: 30, Active: true},
{Name: "Bob", Age: 25, Active: false},
}
for _, person := range people {
fmt.Println(person.Name, "is", person.Age, "years old")
}range just means "loop through every item." The _ means "I don't care about the index number, just give me each item."
5. Pointers — The "Address" Trick
This one scares beginners more than it should. Here's the simplest possible explanation:
A normal variable holds a value, like a piece of paper with a number written on it. A pointer doesn't hold the value itself — it holds the address of the house where the value lives.
Why does this matter? Because sometimes you want a function to actually change your original data, not just look at a copy of it.
go
package main
import "fmt"
func birthday(age *int) { // *int means "a pointer to an int"
*age = *age + 1 // go to that address and update the value there
}
func main() {
myAge := 30
birthday(&myAge) // &myAge means "the address of myAge"
fmt.Println(myAge) // 31 — it actually changed!
}Two symbols to remember:
&variable— "give me the address of this variable" (like getting someone's house address)*pointer— "go to that address and get/set what's actually there"
Without pointers, birthday(myAge) would only get a copy of your age, and the original myAge would never change.
6. Putting It Together — Building a Stack
A stack is a real-world data structure pattern: think of a stack of plates. You always add a plate to the top, and you always remove from the top too. Last one in is the first one out (called LIFO — Last In, First Out).
Go doesn't have a built-in "Stack" type — but a slice does the job perfectly:
go
package main
import "fmt"
type Stack struct {
items []string
}
func (s *Stack) Push(item string) {
s.items = append(s.items, item) // add to the top
}
func (s *Stack) Pop() string {
last := s.items[len(s.items)-1] // grab the top item
s.items = s.items[:len(s.items)-1] // shrink the slice, removing the top
return last
}
func main() {
plates := Stack{}
plates.Push("plate 1")
plates.Push("plate 2")
plates.Push("plate 3")
fmt.Println(plates.Pop()) // "plate 3" — the last one added comes off first
}What's new here:
func (s *Stack) Push(...)— this is a method. It means "this function belongs to theStackstruct." The*Stackmeans it can modify the actual stack, not just a copy (pointers again!).We're combining everything: a struct (the Stack blueprint), a slice inside it (the storage), and pointers (so changes stick).
This is the moment where data structures stop being separate ideas and start clicking together as one toolkit.
Cheat Sheet — Quick Reference
StructureReal-world analogyWhen to use itArrayEgg carton (fixed slots)Rare — when size truly never changesSliceStretchy grocery bagAlmost always — your default "list"MapLabeled filing cabinetFast lookup by a key (name, ID, etc.)StructCustom form/blueprintBundling related fields togetherPointerA house addressLetting functions actually modify the original data
What to Practice Next
A great next step: build a small address book program that uses a slice of Person structs, lets you append new people, and uses a map to quickly look someone up by name. That one exercise touches every concept in this post.
