Go works well for coding interviews if the company lets you use it and you know its gaps in advance. The language is small and compiles fast, but it has no built-in set, heap, deque or ternary, so you assemble those from maps, slices and container/heap. This post lists what to prepare, the Go-specific bugs that cost candidates time, and tested code for the patterns that come up most. All code was run with Go 1.22.
Should you pick Go?
First, confirm the language is allowed. Most companies let you choose from a list, and Go is on many of them, but some interviews or online assessments restrict the options, so ask your recruiter. After that, it is a trade-off:
| In Go’s favour | Against |
|---|---|
| Small language, few ways to do things, readable code | More lines than Python for the same algorithm |
| Static types catch mistakes before you run | No built-in set, heap, deque or stack type |
| Fast, so brute force survives longer on time limits | Interviewers may be less familiar with idioms than with Java or Python |
| The right choice if the job is Go, and interviewers for Go teams expect it | No ternary operator, no while, no generic helpers for everything |
If you are strong in both Python and Go, Python is usually quicker to write for pure algorithm problems. If the role is a Go backend job, use Go: it shows the skill the team hires for. Whatever you pick, write the same language in all your practice so the syntax is automatic.
The data structures you must be able to write quickly
Slices, maps and sets
Slices ([]int) are your arrays, stacks and queues. Maps are your hash tables. A set is a map with an empty-struct value, which takes no space:
seen := make(map[int]struct{})
seen[5] = struct{}{}
if _, ok := seen[5]; ok {
// present
}
// counting
counts := make(map[string]int)
for _, w := range words {
counts[w]++ // missing key reads as 0
}
Here is the standard Two Sum with a map. Note the v, ok form to test for a key:
func twoSum(nums []int, target int) []int {
seen := make(map[int]int)
for i, x := range nums {
if j, ok := seen[target-x]; ok {
return []int{j, i}
}
seen[x] = i
}
return nil
}
// twoSum([]int{2, 7, 11, 15}, 9) -> [0 1]
Stack and queue with slices
A stack is append to push and s = s[:len(s)-1] to pop. A queue is append and q = q[1:]. That queue is fine for interview-sized BFS. For a very long-running queue, re-slicing from the front keeps the old backing array alive, which you can mention to the interviewer and fix with a ring buffer or container/list if asked.
Heap with container/heap
Go has no ready-made priority queue. You implement five methods on a type and call heap.Push and heap.Pop. Memorize this template; it is the part of Go interviews that costs the most time when you have not practiced it.
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] }
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)
x := old[n-1]
*h = old[:n-1]
return x
}
// k-th largest: keep a min-heap of size k
func kthLargest(nums []int, k int) int {
h := &IntHeap{}
for _, x := range nums {
heap.Push(h, x)
if h.Len() > k {
heap.Pop(h)
}
}
return (*h)[0]
}
// kthLargest([]int{3, 2, 1, 5, 6, 4}, 2) -> 5
Push and pop on the heap are O(log n), so this runs in O(n log k). For a max-heap, flip the comparison in Less. For Dijkstra or “merge k lists”, use a struct element type with a priority field and compare on it.
Sorting and searching
Use the library; interviewers rarely want a hand-written sort unless they ask. sort.Slice takes a comparison function, and since Go 1.21 the slices package has slices.Sort, slices.SortFunc and slices.BinarySearch:
keys := []string{"c", "a", "b"}
sort.Slice(keys, func(i, j int) bool {
if counts[keys[i]] != counts[keys[j]] {
return counts[keys[i]] > counts[keys[j]] // higher count first
}
return keys[i] < keys[j] // tie-break for a stable answer
})
idx, found := slices.BinarySearch([]int{1, 3, 5, 7}, 5) // 2 true
pos := sort.SearchInts([]int{1, 3, 5, 7}, 4) // 2 (insert position)
Go 1.21 also added built-in min and max functions and clear (see the Go 1.21 release notes), so you no longer write your own max(a, b) helper. If the judge runs an older Go version, you still need the helper, so check which version you will be on.
Trees and linked lists
Interview platforms provide the node struct. Know that a nil pointer is the empty tree, so a recursive function starts with if node == nil { return ... }. Methods that modify a node by value silently do nothing, so pass pointers. A classic mistake is writing func (t *TreeNode) Insert and assigning to t when it is nil: that changes only the local copy of the pointer. Return the new root instead: root = insert(root, v).
Go-specific pitfalls that cost time
1. Slices share memory, which breaks backtracking
Appending to a slice may or may not reallocate. If you save path to a result inside a backtracking function without copying, later changes overwrite it. This is the most common Go interview bug. Here are the subsets of [1 2 3] collected without and with a copy:
func subsets(nums []int, clone bool) [][]int {
var res [][]int
var path []int
var dfs func(int)
dfs = func(i int) {
if i == len(nums) {
if clone {
res = append(res, slices.Clone(path))
} else {
res = append(res, path)
}
return
}
dfs(i + 1)
path = append(path, nums[i])
dfs(i + 1)
path = path[:len(path)-1]
}
dfs(0)
return res
}
Output without cloning: [[] [2] [2] [1 2] [1] [1 2] [1 2] [1 2 3]]. With slices.Clone: [[] [3] [2] [2 3] [1] [1 3] [1 2] [1 2 3]]. The first is wrong: entries overwrote each other. Before Go 1.21, use append([]int(nil), path...) to copy.
A related surprise: a sub-slice shares its parent’s array.
a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99) // b has spare capacity, so this overwrites a[2]
fmt.Println(a, b) // [1 2 99 4] [1 2 99]
2. Strings are bytes; characters are runes
len(s) counts bytes and s[i] is a byte. For text with non-ASCII characters, convert to []rune before indexing or reversing:
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
s := "héllo"
fmt.Println(len(s), len([]rune(s)), reverse(s), s[1]) // 6 5 olléh 195
Strings are immutable, so building one with += in a loop is O(n²); use strings.Builder. Many interview problems are ASCII-only, in which case bytes are fine, but ask and say so.
3. Maps: reading is safe, writing to a nil map panics
Reading a missing key returns the zero value; use v, ok := m[k] when zero is a legitimate value. A map declared with var m map[string]int is nil: reading from it works (0), but assigning panics. Create maps with make. Iteration order is random, so sort the keys when the output order matters.
4. Integer division, overflow and loop variables
- Division truncates toward zero and
%takes the sign of the dividend:-7/2is-3and-7%2is-1. Fix negative modulo with((x % n) + n) % n. intis 64-bit on 64-bit platforms and overflow wraps silently. For midpoint,lo + (hi-lo)/2is the safe habit.- Before Go 1.22, a closure inside a
forloop captured one shared loop variable. Since Go 1.22 each iteration has its own copy, for modules whosego.modsaysgo 1.22or later (see the Go blog post on loop variables). On an older judge the same code prints different results, so pass the variable as an argument when launching goroutines or building closures.
5. Compile errors for unused names
An unused variable or import is a compile error, not a warning. When you comment out a line during an interview, check what else now goes unused. Use _ for values you ignore.
A graph pattern: BFS on a grid
Shortest path in an unweighted grid is a standard question. A struct as a map key makes the visited set tidy, since structs of comparable fields are comparable:
type P struct{ r, c int }
func shortest(grid []string, s, t P) int {
dist := map[P]int{s: 0}
q := []P{s}
for len(q) > 0 {
cur := q[0]
q = q[1:]
if cur == t {
return dist[cur]
}
for _, d := range []P{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} {
n := P{cur.r + d.r, cur.c + d.c}
if n.r < 0 || n.c < 0 || n.r >= len(grid) || n.c >= len(grid[0]) || grid[n.r][n.c] == '#' {
continue
}
if _, ok := dist[n]; !ok {
dist[n] = dist[cur] + 1
q = append(q, n)
}
}
}
return -1
}
// grid := []string{"..#.", "..#.", "...."}
// shortest(grid, P{0, 0}, P{0, 3}) -> 7
The wall in the middle forces the path down, across and back up: 2 + 3 + 2 = 7 steps. Time and space are O(rows × cols).
Concurrency: only if the role or question asks
Standard algorithm problems do not need goroutines, and using them without reason adds risk. Learn the basics anyway, since Go jobs often ask for one concurrency question: goroutines, channels, sync.WaitGroup and sync.Mutex. This sums two halves of a slice in parallel:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8}
results := make(chan int, 2) // buffered so senders never block
var wg sync.WaitGroup
for _, part := range [][]int{nums[:4], nums[4:]} {
wg.Add(1)
go func(p []int) {
defer wg.Done()
s := 0
for _, v := range p {
s += v
}
results <- s
}(part)
}
wg.Wait()
close(results)
total := 0
for s := range results {
total += s
}
fmt.Println(total) // 36
And a counter that is safe under many goroutines, using a mutex (it printed 1000 on every run, including under go run -race):
var mu sync.Mutex
var wg sync.WaitGroup
count := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count) // 1000
Typical bugs to talk about: forgetting wg.Wait() so main exits early, closing a channel twice, sending on a channel nobody receives from (deadlock), and shared writes without a lock. Mention go run -race as how you would check.
How to prepare
- Do the core patterns in Go. Hash maps, two pointers, sliding window, binary search, BFS/DFS, heap, backtracking, and dynamic programming. Our posts on the sliding window technique and algorithm design patterns explain the ideas; implementing them in Go makes the syntax automatic.
- Keep a personal template file with the heap, a queue, a set, a grid-neighbour loop and a sort with a custom comparator. Retype it until you can write each in a couple of minutes.
- Learn the platform's quirks. Does it give you
package mainandmain, or just a function? Which Go version? Isslicesavailable? - Talk while you code. Say why you chose a map, what the complexity is, and where slices might alias. Idiomatic names and short functions read well.
- Practice under time limits, with the editor you will use. If your target platform is a plain text box, practice without autocomplete.
AlgoCademy's lessons are in Python, JavaScript, Java and C++, not Go, so use them for the ideas, then re-implement the solution in Go, for example in the Binary Search lessons. That translation step is a good test of whether you understand the algorithm or just the Python syntax.
