Go Coding Interview Gotchas: The Footguns Nobody Warns You About
- Slice append inside a function is invisible to the caller: the slice header is a struct that gets copied, not referenced
- Nil interface is not nil when it holds a typed nil pointer; always return untyped
nilfrom error-returning functions - Closure loop variables (pre-Go 1.22) all capture the same final value; shadow the variable or pass it as an argument
- Nil map writes panic at runtime; nil map reads return the zero value safely; always initialize with
make - sort.Slice subtraction comparators silently overflow on large integers; use direct
<comparison instead - String indexing returns a byte, not a rune; convert to
[]runefor Unicode-safe character-level access - Go has no built-in set, priority queue, or ordered map; know the
container/heapboilerplate andmap[T]struct{}workaround before your interview
Go looks approachable. Short programs, fast compilation, a garbage collector that minds its own business. You sit down for your coding interview, write what looks like clean, idiomatic Go, and it panics. Or silently gives you the wrong answer. Or you spend fifteen minutes implementing a heap from scratch because you never quite learned how container/heap works, and now here you are, in an interview, writing interface boilerplate while the interviewer watches.
These are the Go-specific traps that bite even experienced engineers. Not the beginner stuff. The ones that look right until execution day.
A Slice Is Not a Reference. Stop Treating It Like One.
This is the most misunderstood thing in Go, and it costs candidates every single week.
A slice is a three-field struct: a pointer to an underlying array, a length, and a capacity. When you pass a slice to a function, you pass a copy of that struct. The copy points to the same backing array, so mutations to existing elements are visible to the caller. But append inside a function is invisible to the caller unless you return and reassign.
func addThree(s []int) { s = append(s, 3) // caller never sees this } func main() { nums := []int{1, 2} addThree(nums) fmt.Println(nums) // [1 2], not [1 2 3] }
The fix: return the slice. Every function that appends should return the updated slice. This is why append is always s = append(s, x).
There is a second slice trap that's worse, because it doesn't panic. It just quietly destroys your data.
a := []int{1, 2, 3, 4} b := a[:2] // b = [1, 2], shares backing array with a b = append(b, 99) // doesn't allocate; overwrites a[2] fmt.Println(a) // [1 2 99 4]
Appending to b didn't exceed its capacity (it inherited cap=4 from a), so Go wrote straight into a[2]. No warning. No panic. Just wrong data. The fix is a three-index slice: b := a[:2:2] caps b's capacity at 2, forcing a fresh allocation on the next append.

Your nil Check Lied to Your Face
An interface value is nil only when both its type and value fields are nil. A non-nil type with a nil value is a non-nil interface, even when the underlying pointer is nil.
var p *MyError = nil var err error = p // err holds (type=*MyError, value=nil) fmt.Println(err == nil) // false
The dangerous version shows up in real code:
func getError(fail bool) error { var p *MyError = nil if fail { p = &MyError{"something"} } return p // always a non-nil interface, even when p is nil }
The caller checks if err != nil, walks right into the block, and panics trying to use a nil pointer they thought they guarded against. The fix: return an untyped nil. When there is no error, return nil. Never return a typed nil through an interface return type.
Every Goroutine Prints 5. You Wrote 0, 1, 2.
Before Go 1.22, every for loop shared a single variable across all iterations. Closures that captured the loop variable all pointed to the same memory address and read the final value after the loop finished.
// Go 1.21 and earlier funcs := make([]func(), 3) for i := 0; i < 3; i++ { funcs[i] = func() { fmt.Println(i) } } for _, f := range funcs { f() // prints 3 3 3, not 0 1 2 }
The goroutine version is the one interviewers ask about most:
for i := 0; i < 5; i++ { go func() { fmt.Println(i) }() // likely prints 5 5 5 5 5 }
Go 1.22 fixed this. Each iteration now gets its own variable. But interviewers still ask it because the question is really about variable scoping and closure semantics, not version trivia.
The pre-1.22 fix: shadow the variable or pass it as an argument.
for i := 0; i < 3; i++ { i := i // new variable per iteration go func() { fmt.Println(i) }() } // or the argument approach, which is clearer for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }

The universal experience of finding a Go closure bug in production code you wrote six months ago.
Read All You Want. Write Once. Goodbye.
A nil map returns the zero value on reads. Writing to a nil map panics immediately at runtime.
var m map[string]int fmt.Println(m["key"]) // 0, no panic m["key"] = 1 // panic: assignment to entry in nil map
Always initialize with make before writing: m := make(map[string]int). The comma-ok idiom (v, ok := m["key"]) works on nil maps safely too.
The second map trap: iteration order is randomized. Go deliberately shuffles map iteration order on every run to prevent programs from accidentally depending on it. If a problem requires sorted key processing, collect keys into a slice and sort.
Maps are also not safe for concurrent use. Two goroutines reading concurrently are fine. One goroutine writing while another reads or writes causes a data race and a runtime crash. Use sync.RWMutex or sync.Map for maps shared across goroutines.
The Standard Library Said No
Go's standard library is intentionally lean. Three things are conspicuously absent, and each one has a workaround you need to know cold before your interview.

Implementing container/heap for the first time, live, under a timer.
No built-in priority queue. container/heap exists, but it requires you to implement five interface methods (Len, Less, Swap, Push, Pop) on your own type. That is twenty lines of boilerplate before you can insert anything. Default is a min-heap: make Less return h[i] < h[j]. For a max-heap, flip to h[i] > h[j]. Practice writing this at least once before your interview. Once you have the pattern memorized it takes two minutes. The first time you encounter it under pressure is not the moment to learn it.
No built-in set. Use map[T]struct{}. The empty struct occupies zero bytes. Adding an element is set[x] = struct{}{}. Membership check is _, ok := set[x]. For simple use, map[T]bool is more readable at the cost of one byte per entry.
No ordered map or sorted set. If you need O(log n) ordered key access, you implement a BST, or more practically maintain a sorted slice and binary search with sort.SearchInts. For most interview problems a sorted slice is enough.
These gaps are covered in more depth in the Go for coding interviews reference guide.
The Subtraction That Silently Corrupts Your Sort
// Looks right. Is wrong. sort.Slice(nums, func(i, j int) bool { return nums[i]-nums[j] < 0 })
When nums[i] is a large negative number and nums[j] is a large positive number, the subtraction overflows. In Go, integer overflow wraps silently. No panic, no error, just a corrupted comparator result and a sorted output that is wrong in ways that are extremely hard to debug.
Use a direct comparison:
sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
For a plain ascending integer sort, sort.Ints(nums) is the one-liner. Use sort.Slice only when you need custom ordering.
s[1] Is a Byte. Not the Character You Think It Is.
Go strings are byte slices internally. s[i] returns a byte (uint8), not a Unicode character.
s := "héllo" fmt.Println(len(s)) // 6, not 5 (é is 2 bytes in UTF-8) fmt.Println(s[1]) // 169 (second byte of é), not 'e'
For character-level operations, convert to []rune first:
r := []rune(s) fmt.Println(len(r)) // 5 fmt.Println(r[1]) // 'é'
Most interview problems use ASCII-only strings, so byte indexing works fine. But if a problem involves reversing a string or counting characters and mentions Unicode in the constraints, convert to []rune.
For string construction in a loop, use strings.Builder. Naive + concatenation is O(n²) because strings are immutable and each + allocates a new string.
int Is Not int64. You Know This. Then You Forget It.
int in Go is 32 bits on 32-bit platforms and 64 bits on 64-bit platforms. LeetCode and most interview environments are 64-bit, so int behaves like int64 in practice. But it is not guaranteed.
Use int for indices and slice lengths. It matches the built-in index type and avoids noisy conversions. Use int64 explicitly when a value could exceed two billion: large sums, intermediate products, or any time the problem constraints hint at n up to 10^9.
Go integers don't auto-promote. Overflow wraps silently. Multiplying two large int values gives you garbage with no warning. Python's automatic bignum behavior does not transfer here.
Go Coding Interview Gotchas: Quick Reference
| Trap | What actually happens |
|---|---|
| Write to nil map | Runtime panic |
| Read from nil map | Zero value, safe |
| Append inside function | Invisible to caller (header copied) |
| Reslice then append within capacity | Mutates original backing array |
| Nil typed pointer returned as interface | Non-nil interface, err != nil is true |
| Map iteration | Random order every run |
sort.Slice with a[i]-a[j] | Silent overflow, wrong sort |
s[i] on UTF-8 string | Byte, not rune |
| Loop closure over variable (pre-1.22) | All closures see final value |
int on 32-bit platform | 32 bits, wraps at ~2 billion |
If you want to practice catching these bugs under real interview pressure, SpaceComplexity runs voice-based mock interviews with rubric feedback on exactly the kind of silent runtime bugs that look fine in a text editor but fail when executed.