12 essential data structures for Go developers. Each one explained with Big O complexity, animated visuals, and real code samples you can copy.
[n]TFixed-size, contiguous block of memory. Elements are stored sequentially and accessed by index in constant time. The foundation of most other data structures.
arr[0] = 7
Complexity
When to use
// Arrays are fixed-size, value types (copied on assignment)
var nums [5]int // zero-valued: [0,0,0,0,0]
primes := [5]int{2, 3, 5, 7, 11}
primes[0] = 13 // O(1) access
third := primes[2] // O(1) -> 5
// Length is part of the type - [5]int != [3]int
fmt.Println(len(primes)) // 5
// Iterate
for i, v := range primes {
fmt.Printf("index %d: %d\n", i, v)
}
// Arrays are rarely used directly - slices are preferred
// Comparison: arrays support == (slices do not)
fmt.Println(nums == [5]int{}) // true[]T (slice)Resizable array that automatically grows when capacity is exceeded. The most commonly used data structure in most languages. Doubles its internal storage when full, giving amortized O(1) appends.
arr[0] = 7
Complexity
When to use
// Slices are Go's dynamic array (backed by an array)
nums := []int{10, 20, 30}
nums = append(nums, 40) // O(1) amortized
nums = append(nums, 50) // doubles cap when full
nums[1] = 25 // O(1) index access
// Insert at index 2 - O(n)
nums = slices.Insert(nums, 2, 28)
// Delete at index 1 - O(n)
nums = slices.Delete(nums, 1, 2)
// Search
idx := slices.Index(nums, 40) // O(n)
has := slices.Contains(nums, 28) // O(n)
// Sort
slices.Sort(nums) // O(n log n)
fmt.Println(nums, len(nums), cap(nums))[]T (slice)Last-In-First-Out (LIFO) collection. Only the top element is accessible. Used for tracking state that must be unwound in reverse order.
Push 10
Complexity
When to use
// Use a slice as a stack (LIFO)
stack := []int{}
// Push - O(1) amortized
stack = append(stack, 10)
stack = append(stack, 20)
stack = append(stack, 30)
// Peek - O(1)
top := stack[len(stack)-1] // 30
// Pop - O(1)
stack = stack[:len(stack)-1] // [10, 20]
// Interview pattern: valid parentheses
func isValid(s string) bool {
st := []rune{}
pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}
for _, c := range s {
if p, ok := pairs[c]; ok {
if len(st) == 0 || st[len(st)-1] != p { return false }
st = st[:len(st)-1]
} else { st = append(st, c) }
}
return len(st) == 0
}container/listFirst-In-First-Out (FIFO) collection. Elements are added at the back and removed from the front. Fundamental for breadth-first processing.
Enqueue 10
Complexity
When to use
// Slice-based queue (simple, fine for interviews)
queue := []string{}
queue = append(queue, "A") // O(1) enqueue
queue = append(queue, "B")
queue = append(queue, "C")
front := queue[0] // O(1) peek -> "A"
queue = queue[1:] // O(1)* dequeue (re-slicing)
// For production, use container/list (doubly-linked)
import "container/list"
q := list.New()
q.PushBack("task1") // O(1) enqueue
q.PushBack("task2")
elem := q.Front() // O(1) peek
q.Remove(elem) // O(1) dequeue
// Or use a channel as a bounded queue
ch := make(chan int, 100)
ch <- 42 // enqueue (blocks if full)
val := <-ch // dequeue (blocks if empty)map[K]VMaps keys to values using a hash function for near-constant-time lookups. The single most important data structure for coding interviews. Every language has a built-in implementation.
hash("age") = 0
Complexity
When to use
// map[K]V - built-in hash map
m := map[string]int{
"apple": 3,
"banana": 5,
}
m["cherry"] = 2 // O(1) add
m["apple"] = 10 // O(1) update
delete(m, "cherry") // O(1)
// Safe lookup (comma-ok idiom)
val, ok := m["banana"] // O(1)
if ok { fmt.Println(val) } // 5
// Interview pattern: Two Sum
func twoSum(nums []int, target int) [2]int {
seen := map[int]int{}
for i, n := range nums {
need := target - n
if j, ok := seen[need]; ok {
return [2]int{j, i}
}
seen[n] = i
}
return [2]int{-1, -1}
}map[T]struct{}Unordered collection of unique elements. Uses hashing internally for O(1) membership testing. Supports mathematical set operations like union, intersection, and difference.
hash("age") = 0
Complexity
When to use
// map[T]struct{} - idiomatic set (zero-byte values)
set := map[int]struct{}{
1: {}, 2: {}, 3: {}, 4: {}, 5: {},
}
set[6] = struct{}{} // O(1) add
delete(set, 1) // O(1) remove
// Check membership
_, has := set[4] // O(1) -> true
// Union
other := map[int]struct{}{4: {}, 5: {}, 6: {}, 7: {}}
union := maps.Clone(set)
for k := range other { union[k] = struct{}{} }
// Intersection
inter := map[int]struct{}{}
for k := range set {
if _, ok := other[k]; ok { inter[k] = struct{}{} }
}
// Interview pattern: contains duplicate
func hasDup(nums []int) bool {
s := map[int]struct{}{}
for _, n := range nums { if _, ok := s[n]; ok { return true }; s[n] = struct{}{} }
return false
}container/listSequence of nodes where each node points to the next (singly linked) or both next and previous (doubly linked). Efficient insertion and deletion at any known position, but no index-based access.
traversing: 5
Complexity
When to use
// container/list - doubly linked list
import "container/list"
ll := list.New()
ll.PushBack(10) // O(1) append
ll.PushBack(20)
ll.PushFront(5) // O(1) prepend
// Find and insert before - O(n) search, O(1) insert
for e := ll.Front(); e != nil; e = e.Next() {
if e.Value.(int) == 20 {
ll.InsertBefore(15, e) // O(1) given element
break
}
}
// Remove - O(1) given element reference
ll.Remove(ll.Front())
// Iterate: 10 -> 15 -> 20
for e := ll.Front(); e != nil; e = e.Next() {
fmt.Print(e.Value, " ")
}sorted sliceCollection of unique elements maintained in sorted order, typically backed by a balanced binary search tree (red-black tree). Supports range queries and O(log n) min/max.
search(8)
Complexity
When to use
// No built-in sorted set - use sorted slice + sort.Search
import "slices"
sorted := []int{1, 3, 5, 8, 9}
// Binary search - O(log n)
idx, found := slices.BinarySearch(sorted, 5)
fmt.Println(idx, found) // 2, true
// Insert maintaining order - O(n) for shift
pos, _ := slices.BinarySearch(sorted, 4)
sorted = slices.Insert(sorted, pos, 4)
// [1, 3, 4, 5, 8, 9]
min := sorted[0] // O(1) -> 1
max := sorted[len(sorted)-1] // O(1) -> 9
// For O(log n) insert/delete, use a third-party
// red-black tree (e.g., github.com/emirpasic/gods)sorted slice of pairsKey-value pairs maintained in sorted key order, typically backed by a balanced BST. Enables ordered iteration and range lookups that hash maps cannot provide.
search(8)
Complexity
When to use
// No built-in sorted map - sort keys manually
m := map[string]int{
"banana": 2,
"apple": 5,
"cherry": 1,
}
// Collect and sort keys - O(n log n)
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
// Iterate in sorted order
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
// apple: 5, banana: 2, cherry: 1
// For O(log n) sorted map, use a third-party btree
// e.g., github.com/google/btreecontainer/heapCollection where elements are dequeued by priority rather than insertion order. Typically implemented as a binary heap. Essential for shortest-path algorithms and top-K problems.
min-heap
Complexity
When to use
// container/heap - implement heap.Interface
import "container/heap"
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // min-heap
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h; n := len(old)
val := old[n-1]; *h = old[:n-1]
return val
}
// Usage
h := &IntHeap{5, 3, 8, 1}
heap.Init(h) // O(n)
heap.Push(h, 2) // O(log n)
min := heap.Pop(h) // O(log n) -> 1
top := (*h)[0] // O(1) peeksync.MapThread-safe hash map designed for concurrent read/write access from multiple threads. Uses fine-grained locking or lock-free techniques instead of a single global lock.
hash("age") = 0
Complexity
When to use
// sync.Map - concurrent-safe map (no generics)
import "sync"
var m sync.Map
m.Store("hits", 0) // O(1) thread-safe write
val, ok := m.Load("hits") // O(1) thread-safe read
if ok { fmt.Println(val) }
// LoadOrStore - atomic get-or-set
actual, loaded := m.LoadOrStore("sessions", 1)
// Range over all entries (snapshot)
m.Range(func(key, value any) bool {
fmt.Printf("%s: %v\n", key, value)
return true // continue iteration
})
// Delete
m.Delete("hits") // O(1)
// For typed concurrent maps, use mutex + map[K]V
// sync.Map is best for read-heavy, stable-key workloads[]T (slice)Zero-copy view over a contiguous region of memory. Lets you reference a portion of an array or buffer without allocating new memory. Critical for performance-sensitive parsing and processing.
Span[0..3] = [1, 2, 3]
Complexity
When to use
// Slices ARE memory views in Go (like Span)
data := []int{1, 2, 3, 4, 5}
// Zero-copy slice - shares underlying array
slice := data[1:4] // O(1) -> [2, 3, 4]
slice[0] = 20 // mutates data: [1, 20, 3, 4, 5]
// Cap reveals underlying capacity
fmt.Println(len(slice), cap(slice)) // 3, 4
// Full slice expression - limits capacity
safe := data[1:4:4] // O(1) len=3, cap=3
// append on safe won't overwrite data[4]
// Copy to new backing array (breaks sharing)
clone := make([]int, len(slice))
copy(clone, slice) // O(n)
// unsafe.Slice for raw memory views (advanced)
// ptr := unsafe.SliceData(data)
// view := unsafe.Slice(ptr, len(data))Average-case time complexity. * = amortized.
| Structure | Access | Search | Insert | Delete |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Dynamic Array | O(1) | O(n) | O(1)* | O(n) |
| Stack | O(n) | O(n) | O(1)* | O(1) |
| Queue | O(n) | O(n) | O(1)* | O(1) |
| Hash Map | O(1) | O(1) | O(1)* | O(1) |
| Hash Set | N/A | O(1) | O(1)* | O(1) |
| Linked List | O(n) | O(n) | O(1) | O(1) |
| Sorted Set | O(n) | O(log n) | O(log n) | O(log n) |
| Sorted Map | O(log n) | O(log n) | O(log n) | O(log n) |
| Priority Queue | O(n) | O(n) | O(log n) | O(log n) |
| Concurrent Map | O(1) | O(1) | O(1)* | O(1) |
| Memory View | O(1) | O(n) | N/A | N/A |
| I need to... | Use |
|---|---|
| Store items by index, resize dynamically | List / Dynamic Array |
| Map keys to values with O(1) lookup | HashMap / Dictionary |
| Track unique items, check existence in O(1) | HashSet / Set |
| Last-in-first-out (undo, DFS, brackets) | Stack |
| First-in-first-out (BFS, task queues) | Queue |
| Keep elements sorted at all times | SortedSet / TreeSet |
| Process items by priority (Dijkstra, top-K) | PriorityQueue / Heap |
| Insert/delete at a known position in O(1) | LinkedList |
| Sorted key-value pairs | SortedDictionary / TreeMap |
| Thread-safe shared cache | ConcurrentDictionary |
| Slice arrays/strings without copying | Span / Slice / memoryview |
The most commonly used are dynamic arrays (List/ArrayList/vector), hash maps (Dictionary/HashMap/dict), and hash sets. For interviews, also know stacks, queues, trees, and priority queues. These cover 90%+ of coding interview problems.
Start with the dynamic array and hash map. Together they solve the majority of interview problems. Then learn stacks (for DFS, bracket matching) and queues (for BFS). After that, tackle trees, heaps, and graphs.
No. Big O measures algorithmic complexity, not language-specific performance. A hash map lookup is O(1) whether you use Python dict, Java HashMap, or C# Dictionary. Constant factors differ (C++ is faster than Python in wall-clock time), but Big O is the same.
Yes, via container/heap. You implement the heap.Interface (Len, Less, Swap, Push, Pop) on your own type.
Practice with an AI interviewer that asks you to implement, optimize, and explain data structure choices in real time.
Try a free mock interviewarrow_forward