Go Built-In Data Structures for Coding Interviews

- Slices are a three-field struct (pointer, len, cap); appending past capacity allocates a new backing array, so always
copybefore storing a path in backtracking results - Nil map reads return the zero value safely; nil map writes panic — initialize with
makeor a map literal before any write - Map iteration order is randomized every run; extract keys, sort them, then iterate when sorted output is required
container/heaprequires five interface methods andheap.Initbefore use; skippingInitsilently corrupts priority orderingstrings.Builderreplaces+in loops; string concatenation in a loop is O(n²) total allocation across n iterationsslices.SortFunc(Go 1.21+) takes anint-returning comparator and benchmarks ~2.5x faster than the oldersort.Slice- Go has no built-in set or deque; use
map[T]struct{}for sets and a head-pointer slice for O(1) queue operations
Go ships with two data structures worth caring about in a coding interview: slices and maps. That's the whole list. No deque, no priority queue, no ordered map, no set. Channels exist but they're for concurrency, not DSA.
Before you panic: this is survivable. The upside is that building everything from first principles makes you understand what these structures actually are. The downside is that two specific bugs will silently ruin your solution with zero compiler feedback, and if you don't know them going in, you'll spend your interview staring at the output wondering what went wrong.
Those two bugs: the slice aliasing trap in backtracking, and writing to a nil map. They don't panic during your test runs. They just produce wrong answers. Both are covered below.
The Slice Is Not What You Think It Is
The slice looks like an array. It acts like an array. It is not an array. Every slice in Go is a three-field struct: a pointer to a backing array, a len, and a cap. When you pass a slice to a function, you copy the struct. The function sees the same backing array through the copied pointer.
s := make([]int, 3, 6) // len=3, cap=6, pointer to array of 6 ints
Appending within capacity updates the backing array in place. Appending past capacity allocates a new array, copies everything over, and returns a new slice header pointing to the new location. The original slice still points to the old array, completely unaware of the move. Like a forwarding address that nobody filed.
Growth strategy: Go doubles capacity for small slices (cap < 256). Above that the runtime uses roughly 1.25 * oldCap + 192, so growth slows at scale. This is why append is amortized O(1) and not O(1) in the worst case. The same amortization math applies to Java's ArrayList and Python's list.
The Reslice Memory Trap
big := make([]int, 0, 100000) // fill big with 100,000 elements... small := big[0:100] // 100 elements visible, 100,000 elements alive
small looks cheap. It keeps the entire backing array alive because it holds a reference to it. Your function returns, you think you freed memory. You did not. The garbage collector cannot collect what small is still pointing at.
If you need to detach, copy explicitly:
small := append([]int(nil), big[0:100]...)
Go 1.2 added three-index slices to cap sharing without a copy: big[0:100:100] limits small's capacity to 100, so the next append on small won't silently write into big's territory.
The Backtracking Bug That Will Haunt You
This is the most common silent bug in Go interview code. It is very sneaky. When you store a path slice in your results and keep mutating it, every stored slice shares the same backing array. At the end of your backtracking run, all your "stored" results are pointing at the same memory, which now reflects the state of path at the last recursive call.
// Wrong: all stored results point to the same backing array results = append(results, path) // Correct: copy before storing tmp := make([]int, len(path)) copy(tmp, path) results = append(results, tmp)
It won't panic. No compiler error. You'll only notice when the output is all-empty or all-identical slices. By then you've already spent five minutes convinced the logic was wrong. Copy first, always.
The Map That Will Panic at the Worst Moment
Maps in Go are hash tables. Average O(1) for get, set, and delete. The full analysis of what that guarantee actually costs is in the hash map time complexity breakdown.
Here is the rule that will save you in an interview: reading from a nil map is safe and returns the zero value. Writing to a nil map panics.
var m map[string]int // nil map _ = m["key"] // fine, returns 0 m["key"] = 1 // panic: assignment to entry in nil map
Go made this choice deliberately. A nil map is a valid read-only map (useful for zero values), but writes require an initialized structure. This sounds reasonable until it's 11pm and you're debugging a runtime panic that your test cases didn't catch because you never wrote to the map in the happy path.
Initialize with make or a map literal before any write. Always.
The Comma-OK Idiom
val, ok := m["key"] if !ok { // key is absent, not just zero-valued }
Without the second return, you can't distinguish a missing key from a key that holds the zero value. This matters constantly: counting frequencies, tracking visited nodes, checking membership in a set. A frequency map that returns 0 for a missing key looks exactly like a frequency map with a key that has count 0. Only ok tells you which one you have.
Iteration Order Is Deliberately Random
Go randomizes map iteration order on every run. This is not a bug. The Go spec requires it to prevent code from accidentally relying on hash-bucket ordering, which changes between Go versions. They made it random specifically so that if you write code that depends on order, it fails loudly and immediately rather than mysteriously breaking after a version upgrade.
If you need sorted output, extract the keys, sort them, then iterate.
keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println(k, m[k]) }
Map as a Set
Go has no built-in set. The idiomatic approach is map[T]struct{}. The empty struct uses zero bytes of storage, which makes it the correct choice over map[T]bool on both semantic and efficiency grounds.
seen := map[int]struct{}{} seen[42] = struct{}{} _, ok := seen[42] // ok == true
Verbose. Yes. Accept it.
String Concatenation Is a Performance Trap
Strings in Go are immutable byte sequences. Every + allocates a new string and copies both sides into it. In a loop that's O(n²) total allocation, which shows up on large inputs in a way that is embarrassing to explain to an interviewer.
// O(n²): each iteration allocates a new string result := "" for _, word := range words { result += word } // O(n): single buffer, one final copy at String() var b strings.Builder for _, word := range words { b.WriteString(word) } result := b.String()
Use strings.Builder for any string you're constructing incrementally. For character-level work, convert to []byte, operate on it, then convert back. Each conversion allocates, so avoid doing it inside a tight inner loop.
Sorting: Two APIs, and One Is Better
The sort package has sort.Ints, sort.Strings, and sort.Slice. Go 1.21 added the slices package with generic, type-safe versions that run roughly 2.5x faster in benchmarks.
import "slices" nums := []int{5, 2, 8, 1} slices.Sort(nums) // ascending, in place // Custom comparator: return negative if a comes before b slices.SortFunc(nums, func(a, b int) int { return b - a // descending })
The old sort.Slice used an untyped func(i, j int) bool closure. slices.SortFunc enforces types at compile time, catches comparator mistakes before runtime, and doesn't need you to remember the index-based signature. If the runtime is Go 1.21 or later, use slices.SortFunc. LeetCode runs Go 1.21 as of 2026.
// Pre-1.21 fallback sort.Slice(pairs, func(i, j int) bool { return pairs[i][0] < pairs[j][0] })
Priority Queue: Meet Your Five New Methods
Go provides container/heap. It is not a heap type. It is a package of heap operations that work on any value implementing a five-method interface. You write all five. Go handles the rest.
The five methods are Len, Less, and Swap (from sort.Interface), plus Push and Pop. Every time you need a heap in an interview, you type these out.
type MinHeap []int func (h MinHeap) Len() int { return len(h) } func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] } func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *MinHeap) Push(x any) { *h = append(*h, x.(int)) } func (h *MinHeap) Pop() any { old := *h n := len(old) x := old[n-1] *h = old[:n-1] return x } h := &MinHeap{5, 2, 8} heap.Init(h) // required: establishes the heap invariant heap.Push(h, 1) top := heap.Pop(h).(int) // 1
heap.Init is not optional. If you skip it and start calling heap.Push on a raw slice, the heap invariant won't hold and priorities will be wrong in ways that are surprisingly hard to debug. The values come out in the wrong order and the bug looks like a logic error, not a missing initialization call.
For a max-heap, flip Less: return h[i] > h[j]. The underlying algorithm is the same one covered in the heap data structure guide. The boilerplate is genuinely painful. Interviewers who use Go know this and won't penalize you for the setup time, but you need to get it right.
Stack, Queue, Deque: It's Slices All the Way Down
Stack is a slice with append and a reslice on pop. Three lines.
stack := []int{} stack = append(stack, v) // push top := stack[len(stack)-1] // peek stack = stack[:len(stack)-1] // pop
Queue is trickier. The naive queue = queue[1:] does not copy data, but it advances the slice header into the array, so the memory from dequeued elements is never reclaimed. Enqueue enough items and dequeue them all, and you've leaked the entire backing array. Use a head pointer instead.
queue := []int{1, 2, 3} head := 0 val := queue[head]; head++ // dequeue: O(1) queue = append(queue, 4) // enqueue: O(1) amortized
When head is past half the slice, compact: queue = append(queue[:0], queue[head:]...); head = 0. For most interview problems the queue stays small enough that this compaction never fires, but knowing why the naive approach accumulates garbage is the kind of thing an interviewer will follow up on.
Deque with O(1) at both ends: container/list is Go's built-in doubly-linked list. It's most useful when you need constant-time removal of an arbitrary node, as in an LRU cache. For sliding window maximum, a circular slice with head and tail pointers is usually cleaner and avoids pointer overhead.
What Go 1.21 Finally Gave You
Three new built-ins that eliminate the most annoying boilerplate:
fmt.Println(min(3, 7)) // 3, works on any ordered type (int, float, string) fmt.Println(max(3, 7)) // 7 clear(m) // deletes all entries from a map; zeroes all elements of a slice
No more if a < b { return a } spread across every function that needs a min. The maps package adds utilities you'd otherwise write by hand:
import "maps" clone := maps.Clone(m) // shallow copy of the map maps.DeleteFunc(m, func(k, v int) bool { return v == 0 }) // conditional delete
Go Data Structures: Quick Reference
| Need | Use | Primary Gotcha |
|---|---|---|
| Dynamic array | []T | Append aliasing; copy before storing in backtracking |
| Hash map | map[K]V | Write to nil map panics; iteration order is random |
| Set | map[T]struct{} | Verbose but zero-byte values |
| Stack | []T + append/reslice | Forgetting the reslice on pop |
| Queue | []T + head pointer | Naive reslice accumulates memory |
| Priority queue | container/heap | Five methods required; heap.Init is not optional |
| String building | strings.Builder | Concat in a loop is O(n²) |
| Sorting | slices.SortFunc (1.21+) | Comparator returns int, not bool |
| Sorted key iteration | Extract, sort, iterate | No ordered map built-in |
| Arbitrary node removal | container/list | Pointer bookkeeping required |
Practice Out Loud
Go's data structure gaps don't hurt you in isolation. They hurt when you're managing time pressure, walking through your algorithm in real time, and your backtracking output is all-empty slices and you have no idea why. The aliasing bug and the nil map panic are the two most common reasons Go candidates lose points quietly, not noisily.
If you want to hear how you sound explaining a priority queue setup or defending your queue implementation under follow-up questions, SpaceComplexity runs AI-powered voice mock interviews with per-dimension rubric feedback. These are exactly the moments where a live session reveals gaps that grinding more problems never will.
For a broader reference covering Go idioms, common patterns, and standard library calls, see the Go for Coding Interviews cheat sheet.
Further Reading
- Go Slices: usage and internals, official Go blog, the authoritative explanation of the three-field header
- container/heap package, interface definition and the canonical priority queue example from the Go team
- slices package, generic sort, search, and comparison functions added in Go 1.21
- Go 1.21 Release Notes, min, max, clear, and the new standard library packages
- Go slice gotchas, hands-on examples of aliasing and memory traps