Somewhere between your first "hello world" and shipping a real service, every Go developer runs into the same question: how do I actually organize my data? The good news is that Go keeps its toolbox refreshingly small. There's no ArrayList versus LinkedList versus Vector paralysis here, just a handful of built-in primitives and a language philosophy that trusts you to build anything fancier yourself.
Grab your favorite drink. We're going to tour every data structure Go gives you out of the box, arrays, slices, maps, structs, pointers, and channels, and then see how the classics like stacks, queues, linked lists, and trees get built on top of them.
Arrays: The Rigid Older Sibling
An array in Go has a fixed size that is baked into its type. An array of 5 ints is a completely different type from an array of 6 ints. This surprises newcomers, but it is exactly why arrays are so predictable: their size is known at compile time, with zero surprise memory growth.
var scores [5]int
scores[0] = 100
fmt.Println(scores) // [100 0 0 0 0]
primes := [4]int{2, 3, 5, 7}
fmt.Println(len(primes)) // 4
In practice, you will rarely reach for a raw array directly. It mostly exists as the foundation slices are built on, but it is worth knowing it is there, quietly guaranteeing a fixed length.
Slices: Arrays That Went to the Gym
If arrays are the rigid older sibling, slices are the cool one everyone actually hangs out with. A slice is a small header made of a pointer to an underlying array, a length, and a capacity, and it gives you a dynamically sized, resizable view over data. This is the workhorse structure of everyday Go code.
nums := []int{1, 2, 3}
nums = append(nums, 4, 5)
fmt.Println(nums, len(nums), cap(nums))
// Slicing creates a new header over the same array
sub := nums[1:3]
sub[0] = 99 // this mutates nums too!
fmt.Println(nums)
That last example is the classic Go gotcha: slicing does not copy data. Two slices can quietly share the same backing array, so a mutation through one is visible through the other. When you truly need an independent copy, reach for the built-in copy function instead.
original := []int{1, 2, 3}
clone := make([]int, len(original))
copy(clone, original)
One more habit worth building early: when you roughly know how many elements you will store, pre-allocate with make so append does not have to keep reallocating a bigger array behind the scenes.
Maps: Go's Built-in Dictionary
A map gives you key-value storage with average constant-time lookups, backed by a hash table. Keys can be any comparable type, strings, ints, structs without slice or map fields, and so on.
ages := map[string]int{"amy": 30, "bo": 25}
ages["cara"] = 41
if age, ok := ages["amy"]; ok {
fmt.Println("amy is", age)
}
delete(ages, "bo")
Notice the comma-ok idiom above. It is how Go tells you whether a key was actually present, rather than silently handing you a zero value. Also worth remembering: map iteration order is intentionally randomized, so never rely on it for anything deterministic.
Structs: Building Your Own Types
Go does not have classes, and it turns out you do not miss them much. A struct is a typed collection of fields, and you attach behavior to it with methods defined separately. It is composition over inheritance, baked right into the syntax.
type User struct {
Name string
Age int
}
func (u User) Greet() string {
return "Hi, I'm " + u.Name
}
u := User{Name: "Amy", Age: 30}
fmt.Println(u.Greet())
Structs can also embed other structs, which gives you a form of composition where the outer type automatically gains the inner type's fields and methods. It looks a bit like inheritance from the outside, but underneath it is just plain field embedding.
Pointers: Sharing Without Copying
Everything in Go is passed by value by default, including structs, which get fully copied when passed to a function. Pointers are how you opt out of that copying and let a function see and modify the original data.
func birthday(u *User) {
u.Age++
}
u := User{Name: "Amy", Age: 30}
birthday(&u)
fmt.Println(u.Age) // 31
Pointers are also what make linked structures possible at all. A struct cannot contain itself directly, but it can absolutely contain a pointer to another instance of itself, and that one idea is the seed of linked lists and trees.
Channels: Data Structures for Talking Goroutines
Channels are Go's most unusual data structure: a typed, concurrency-safe pipe for passing values between goroutines. Where the previous structures organize data at rest, channels organize data in motion.
ch := make(chan int, 3) // buffered channel
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
An unbuffered channel forces sender and receiver to rendezvous at the same instant, while a buffered channel behaves like a small fixed-size queue that lets the sender get a head start.
Building the Classics on Top
Go deliberately ships without a Stack, Queue, LinkedList, or Tree type in its core syntax. Instead, it gives you slices, maps, structs, and pointers, and expects you to assemble the classics yourself. That is not a gap, it is a design choice, and once you see the patterns, they take only a few lines each.
Stack: Last In, First Out
A slice already gives you push and pop for free, since append and re-slicing operate naturally on the end of the underlying array.
type Stack []int
func (s *Stack) Push(v int) {
*s = append(*s, v)
}
func (s *Stack) Pop() (int, bool) {
old := *s
if len(old) == 0 {
return 0, false
}
v := old[len(old)-1]
*s = old[:len(old)-1]
return v, true
}
Queue: First In, First Out
A queue is the same idea, just popping from the front instead of the back. For high-throughput queues, container/list or a ring buffer avoids the cost of shifting elements, but a slice is perfectly fine for everyday use.
type Queue []int
func (q *Queue) Enqueue(v int) {
*q = append(*q, v)
}
func (q *Queue) Dequeue() (int, bool) {
old := *q
if len(old) == 0 {
return 0, false
}
v := old[0]
*q = old[1:]
return v, true
}
Linked List: Pointers All the Way Down
This is where pointers earn their keep. Each node holds a value and a pointer to the next node, and the list itself is just a pointer to the first one.
type Node struct {
Value int
Next *Node
}
type List struct {
Head *Node
}
func (l *List) Prepend(v int) {
l.Head = &Node{Value: v, Next: l.Head}
}
Go's standard library even ships a doubly linked list in container/list, for the times you would rather not hand-roll one.
Trees: Nodes With More Than One Child
A binary tree is just a node struct with two self-referencing pointers instead of one. Swap Next for Left and Right, and you already know how to build it.
type TreeNode struct {
Value int
Left, Right *TreeNode
}
func (t *TreeNode) Insert(v int) *TreeNode {
if t == nil {
return &TreeNode{Value: v}
}
if v < t.Value {
t.Left = t.Left.Insert(v)
} else {
t.Right = t.Right.Insert(v)
}
return t
}
Sets: Maps in Disguise
Go has no built-in set type, but a map with an empty struct value gets you one for free, and the empty struct costs zero bytes per entry.
set := map[string]struct{}{}
set["go"] = struct{}{}
_, exists := set["go"]
fmt.Println(exists) // true
Heaps: Priority Queues via an Interface
The container/heap package takes a different approach from the rest of this list: instead of a ready-made type, it gives you an interface. You implement Len, Less, Swap, Push, and Pop on your own slice-backed type, and the package turns it into a fully working priority queue with heap.Push and heap.Pop.
Quick Cheat Sheet
Array: fixed size, value type, rarely used directly
Slice: dynamic, resizable view over an array, your daily driver
Map: hash table, key-value pairs, random iteration order
Struct: your own custom types, composition over inheritance
Pointer: share data without copying, enables self-referencing structures
Channel: safe data flow between goroutines
Stack, Queue, Linked List, Tree, Set, Heap: all assembled from the six primitives above
Wrapping Up
Go's minimalism is not laziness, it is a bet that a small set of well-designed primitives beats a sprawling standard library of container types. Once arrays, slices, maps, structs, pointers, and channels feel natural, you will find that stacks, queues, linked lists, trees, sets, and heaps stop feeling like separate things to memorize and start feeling like the same handful of ideas, recombined. Now go build something with them.
