← Writing
Go Lang
July 7, 2026 · 8 min read38
go

Go Data Structures, Explained Like You're Five (Sort Of)

A beginner-friendly walkthrough of arrays, slices, maps, structs, and pointers in Go — with real code and zero assumed knowledge.

Abdulboriy Malikov

Go Data Structures, Explained Like You're Five (Sort Of)

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:

  1. Arrays — the rigid box

  2. Slices — the flexible box (the one you'll actually use)

  3. Maps — the labeled drawer

  4. Structs — the custom-built container

  5. Pointers — the "address" that lets containers talk to each other

  6. 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 called eggs. 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: append doesn't change the bag in place, it hands you back a (possibly new) bag, so you always write fruits = 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 called Person. It has three fields: Name, Age, Active."

  • p := Person{...} — "Fill out that form and call the filled-out copy p."

  • p.Name — "Look at the Name field 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 the Stack struct." The *Stack means 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.