Source notes
- The Go Programming Language SpecificationArray, slice, map, string, comparison, integer, assignment, and range semantics.
- Go slices: usage and internalsSlice descriptors, shared backing arrays, append behavior, capacity, copying, and allocation.
- Strings, bytes, runes and characters in GoUTF-8 string storage, byte indexing, rune decoding, range iteration, and invalid encoding behavior.
- container/heap packageheap.Interface, Init, Push, Pop, Fix, and Remove contracts.
- sort packageSlice ordering, stability distinctions, Search, and SearchInts behavior.
Input and output conventions#
- Function interview
- Implement the requested function against interviewer-supplied types. Confirm whether inputs may be mutated, whether nil and empty slices are equivalent for the result, and whether output order matters. Avoid package main, scanning, or printing unless the environment asks for them.
- Online assessment
- Read the exact runner contract first. When stdin/stdout is required, use bufio.Reader or Fscan for token input and a buffered Writer for output; defer writer.Flush(). Do not assume a platform helper, third-party module, or custom node type exists unless the prompt supplies it.
Quick Reference#
The syntax candidates most often want immediately before a coding round.
nums := make([]int, n)
nums = append(nums, value)
last := nums[len(nums)-1]
nums = nums[:len(nums)-1]
clone := append([]int(nil), nums...)
window := nums[left:right] // shares nums' backing arrayfrequency := make(map[int]int)
frequency[value]++
count, present := frequency[value]
_ = count
seen := make(map[int]struct{})
seen[value] = struct{}{}
_, exists := seen[value]
_ = present
_ = existsstack := []int{}
stack = append(stack, value)
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
queue := []int{start}
for head := 0; head < len(queue); head++ {
current := queue[head]
_ = current
}
slices.Sort(nums) // Go 1.21+
slices.SortFunc(items, func(a, b Item) int {
if a.Priority != b.Priority { return cmp.Compare(a.Priority, b.Priority) }
return cmp.Compare(a.Sequence, b.Sequence)
})The sorting fragment needs the standard-library cmp and slices packages and an Item type with Priority and Sequence fields.
byteLength := len(text)
runes := []rune(text)
runeCount := len(runes)
for byteOffset, r := range text {
_, _ = byteOffset, r
}
in := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
fmt.Fscan(in, &n)
fmt.Fprintln(out, answer)The I/O fragment needs bufio, fmt, and os; len(string) counts bytes, not Unicode code points.
Go interview basics and runner contracts#
Keep the function signature and types the prompt gives you. Go makes zero values useful, but nil slices, empty slices, missing map keys, and zero values are not interchangeable in every API contract.
func TwoSum(nums []int, target int) []int {
position := make(map[int]int)
for index, value := range nums {
if prior, ok := position[target-value]; ok {
return []int{prior, index}
}
position[value] = index
}
return nil
}- Use short declarations when they clarify ownership, but do not accidentally shadow a result or error in an inner block.
- Multiple assignment makes swaps direct: left, right = right, left.
- A nil slice has length zero and can be ranged over or appended to; confirm whether the caller distinguishes nil from an allocated empty slice in serialized output or equality checks.
Arrays, slices, append, and mutation#
An array value has its length in its type. A slice is a descriptor over a backing array, so subslices can alias each other and append may either reuse that array or allocate a new one depending on capacity.
base := []int{1, 2, 3, 4}
view := base[1:3] // [2, 3], shares base
view[0] = 9 // base is now [1, 9, 3, 4]
independent := make([]int, len(view))
copy(independent, view)
independent[0] = 7 // base remains unchanged- append returns the resulting slice header; assign its return value.
- Use copy or append([]T(nil), source...) when the output must not share mutable storage with the input.
- A tiny subslice can retain a large backing array. Copy the live portion when a long-lived result should release the larger storage.
| Operation | Complexity | Interview note |
|---|---|---|
slice[i] | O(1) | Not available |
append(slice, x) | O(1) amortized | An individual growth can allocate and copy O(n) elements. |
copy(dst, src) | O(k) | k is the number of copied elements. |
slice[a:b] | O(1) | Creates another descriptor and normally shares the backing array. |
Maps, sets, comma-ok, and equality#
Map lookup returns the value type's zero value when the key is absent. Use the comma-ok form whenever an absent key must differ from a present key storing zero.
type Cell struct {
Row int
Col int
}
distance := map[Cell]int{{Row: 0, Col: 0}: 0}
value, present := distance[Cell{Row: 0, Col: 1}]
seen := make(map[Cell]struct{})
seen[Cell{Row: 2, Col: 3}] = struct{}{}
_, visited := seen[Cell{Row: 2, Col: 3}]
_, _, _ = value, present, visited- Comparable types can be map keys. Slices, maps, and functions are not comparable except to nil; an array or struct key is valid only when all of its components are comparable.
- Use map[T]struct{} for a set when only membership matters.
- Map iteration order is unspecified. Collect and sort keys when output or tie-breaking order is observable.
- Slices require element-by-element comparison or slices.Equal; == is not available for two slices.
| Operation | Complexity | Interview note |
|---|---|---|
map[key] lookup | O(1) expected | Do not promise a worst-case constant bound. |
map[key] = value | O(1) expected | Not available |
delete(map, key) | O(1) expected | Not available |
Stacks and queues with slices#
A slice supports a stack directly. For FIFO work, keep a head index instead of repeatedly deleting index zero and shifting elements.
queue := []*Node{start}
for head := 0; head < len(queue); head++ {
node := queue[head]
queue[head] = nil // release the consumed reference
for _, next := range node.Neighbors {
queue = append(queue, next)
}
}
// In a long-lived queue, compact after a meaningful consumed prefix:
// queue = append([]*Node(nil), queue[head:]...); head = 0- Popping a stack uses value := stack[len(stack)-1] followed by stack = stack[:len(stack)-1]. Guard the empty case first.
- A head index gives O(1) dequeue work and O(n) total traversal; occasional proportional compaction remains amortized efficient.
- For pointer-containing elements, clear consumed slots before a long-lived backing array retains them unnecessarily.
Strings, bytes, runes, and Unicode assumptions#
A Go string is a read-only byte sequence that conventionally contains UTF-8. len(text) counts bytes, indexing returns one byte, and range decodes runes while reporting each rune's starting byte offset.
text := "aé"
fmt.Println(len(text)) // 3 bytes
fmt.Println(len([]rune(text))) // 2 Unicode code points
for byteOffset, r := range text {
fmt.Printf("%d %U
", byteOffset, r)
}- Use []byte when the contract is ASCII or raw bytes and state that assumption.
- Use []rune when indexing or reversing Unicode code points; this still does not implement grapheme-cluster segmentation or normalization.
- Strings can contain arbitrary bytes. range replaces invalid UTF-8 encodings with RuneError while advancing according to the decoder rules.
- Use strings.Builder or a byte/rune buffer for repeated construction instead of repeated immutable concatenation in a loop.
| Operation | Complexity | Interview note |
|---|---|---|
len(text) | O(1) | Returns bytes, not rune count. |
[]rune(text) | O(n) time and space | n is the byte length decoded into code points. |
text[a:b] | O(1) | Slices byte boundaries and shares the underlying string data; boundaries may split UTF-8. |
Sorting, search, comparisons, and ties#
Use the standard sort or slices packages and express the full ordering contract. A deterministic secondary key is required when equal primary keys can change the observable answer.
slices.SortFunc(items, func(a, b Item) int {
if byPriority := cmp.Compare(a.Priority, b.Priority); byPriority != 0 {
return byPriority
}
return cmp.Compare(a.Sequence, b.Sequence)
})
index := sort.Search(len(nums), func(i int) bool {
return nums[i] >= target
})- slices.SortFunc and cmp.Compare are available in the Go 1.22+ compatibility target.
- sort.Search expects a monotonic predicate: false values followed by true values. State that proof before using it.
- Use slices.SortStableFunc or sort.SliceStable only when preserving original order for equal keys is part of the contract.
| Operation | Complexity | Interview note |
|---|---|---|
slices.Sort(values) | O(n log n) comparisons | Comparison cost may add another factor for compound values such as strings. |
sort.Search(n, predicate) | O(log n) predicate calls | Multiply by the cost of one predicate call. |
Integer width and arithmetic#
int is either 32 or 64 bits depending on the implementation. Use int for valid indices and sizes, but choose int64 when the prompt's sums, products, distances, or counts require an explicit 64-bit range.
sum := int64(a) + int64(b)
product := int64(width) * int64(height)
mid := left + (right-left)/2
const MaxInt64 = int64(^uint64(0) >> 1)
_ = []int64{sum, product, int64(mid), MaxInt64}- Converting after an overflowing int operation is too late; widen each operand before addition or multiplication.
- Integer overflow follows Go's defined integer-operation semantics but almost always violates an interview algorithm's mathematical contract.
- Keep index arithmetic in int when indexing slices, and convert deliberately at the numeric boundary rather than scattering casts.
Priority queues with container/heap#
container/heap supplies heap algorithms over a type that implements sort.Interface plus Push and Pop. Call heap.Init once for existing data, then use heap.Push and heap.Pop so swaps restore the invariant.
type Entry struct {
Priority int
Sequence int
Value string
}
type EntryHeap []Entry
func (h EntryHeap) Len() int { return len(h) }
func (h EntryHeap) Less(i, j int) bool {
if h[i].Priority != h[j].Priority {
return h[i].Priority < h[j].Priority
}
return h[i].Sequence < h[j].Sequence
}
func (h EntryHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *EntryHeap) Push(x any) { *h = append(*h, x.(Entry)) }
func (h *EntryHeap) Pop() any {
old := *h
last := old[len(old)-1]
old[len(old)-1] = Entry{}
*h = old[:len(old)-1]
return last
}
h := &EntryHeap{}
heap.Init(h)
heap.Push(h, Entry{Priority: 2, Sequence: 0, Value: "task"})
next := heap.Pop(h).(Entry)The fragment needs container/heap. Push and Pop methods manage the backing slice; callers use heap.Push and heap.Pop to preserve ordering.
| Operation | Complexity | Interview note |
|---|---|---|
heap.Init(h) | O(n) | Not available |
heap.Push / heap.Pop | O(log n) | Not available |
(*h)[0] | O(1) | Guard Len first; index zero is the minimum under Less. |
heap.Fix | O(log n) | Use after an indexed item's priority changes. |
Common Interview Templates#
These are structural recall aids, not answers to proprietary problems. Open only the pattern you need.
Frequency mapCounts or earlier occurrences replace repeated scans.
Use when: Counts or earlier occurrences replace repeated scans.
Typical complexity: O(n) expected time and O(k) space for k distinct values.
func Frequencies(values []int) map[int]int {
counts := make(map[int]int, len(values))
for _, value := range values {
counts[value]++
}
return counts
}Queue BFSYou need reachability, layers, or shortest unweighted distance.
Use when: You need reachability, layers, or shortest unweighted distance.
Typical complexity: O(V + E) time and O(V) space for an adjacency list.
func BFS(graph [][]int, start int) []int {
if start < 0 || start >= len(graph) {
return nil
}
seen := make([]bool, len(graph))
seen[start] = true
queue := []int{start}
order := make([]int, 0, len(graph))
for head := 0; head < len(queue); head++ {
node := queue[head]
order = append(order, node)
for _, next := range graph[node] {
if !seen[next] {
seen[next] = true
queue = append(queue, next)
}
}
}
return order
}Iterative DFSYou need depth-first reachability without depending on call-stack depth.
Use when: You need depth-first reachability without depending on call-stack depth.
Typical complexity: O(V + E) time and O(V) explicit-stack space.
func DFS(graph [][]int, start int) []int {
if start < 0 || start >= len(graph) {
return nil
}
seen := make([]bool, len(graph))
stack := []int{start}
order := make([]int, 0, len(graph))
for len(stack) > 0 {
last := len(stack) - 1
node := stack[last]
stack = stack[:last]
if seen[node] {
continue
}
seen[node] = true
order = append(order, node)
for i := len(graph[node]) - 1; i >= 0; i-- {
next := graph[node][i]
if !seen[next] {
stack = append(stack, next)
}
}
}
return order
}Lower boundA sorted slice needs the first index whose value is at least the target.
Use when: A sorted slice needs the first index whose value is at least the target.
Typical complexity: O(log n) time and O(1) space.
func LowerBound(nums []int, target int) int {
left, right := 0, len(nums)
for left < right {
mid := left + (right-left)/2
if nums[mid] < target {
left = mid + 1
} else {
right = mid
}
}
return left
}Size-k min-heapYou only need the k largest values seen so far, not a full ordering.
Use when: You only need the k largest values seen so far, not a full ordering.
Typical complexity: O(n log k) time and O(k) space.
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
value := old[len(old)-1]
*h = old[:len(old)-1]
return value
}
func LargestK(values []int, k int) []int {
if k <= 0 {
return []int{}
}
h := &IntHeap{}
heap.Init(h)
for _, value := range values {
heap.Push(h, value)
if h.Len() > k {
heap.Pop(h)
}
}
return append([]int(nil), (*h)...)
}Union-findEdges repeatedly merge components and connectivity queries interleave with additions.
Use when: Edges repeatedly merge components and connectivity queries interleave with additions.
Typical complexity: Near-constant amortized operations with path compression and union by size.
type UnionFind struct {
parent []int
size []int
}
func NewUnionFind(n int) *UnionFind {
u := &UnionFind{parent: make([]int, n), size: make([]int, n)}
for i := 0; i < n; i++ {
u.parent[i], u.size[i] = i, 1
}
return u
}
func (u *UnionFind) Find(node int) int {
if u.parent[node] != node {
u.parent[node] = u.Find(u.parent[node])
}
return u.parent[node]
}
func (u *UnionFind) Union(a, b int) bool {
rootA, rootB := u.Find(a), u.Find(b)
if rootA == rootB {
return false
}
if u.size[rootA] < u.size[rootB] {
rootA, rootB = rootB, rootA
}
u.parent[rootB] = rootA
u.size[rootA] += u.size[rootB]
return true
}BacktrackingYou must enumerate choices and restore the partial state before exploring a sibling branch.
Use when: You must enumerate choices and restore the partial state before exploring a sibling branch.
Typical complexity: O(n·2ⁿ) time including copied subset output and O(n) active-path space.
func Subsets(values []int) [][]int {
answers := make([][]int, 0, 1<<len(values))
path := make([]int, 0, len(values))
var visit func(int)
visit = func(index int) {
if index == len(values) {
answers = append(answers, append([]int(nil), path...))
return
}
visit(index + 1)
path = append(path, values[index])
visit(index + 1)
path = path[:len(path)-1]
}
visit(0)
return answers
}1-D dynamic programmingEach state depends on a small, already-computed set of earlier states.
Use when: Each state depends on a small, already-computed set of earlier states.
Typical complexity: O(n) time and O(1) auxiliary space.
func MaxNonAdjacent(values []int) int {
previousTwo, previousOne := 0, 0
for _, value := range values {
withCurrent := previousTwo + value
current := previousOne
if withCurrent > current {
current = withCurrent
}
previousTwo, previousOne = previousOne, current
}
return previousOne
}Common Interview Mistakes#
Assuming map iteration is ordered
Go deliberately leaves map iteration order unspecified. Sort keys or results when order is observable.
Confusing bytes and runes
len and indexing operate on bytes; range decodes runes and reports byte offsets.
Forgetting slice aliasing
Subslices share a backing array, and append may reuse it. Copy when results must be independent.
Retaining consumed queue entries
A long-lived head-index queue can retain its backing array and pointer values. Clear consumed slots and compact deliberately.
Ignoring comma-ok
A missing key and a stored zero produce the same single lookup value.
Calling heap methods directly
Use heap.Init, heap.Push, and heap.Pop; the interface methods are backing-storage hooks.
Using int without a range check
int width is implementation-specific. Convert operands to int64 before arithmetic when the contract requires it.
Forgetting to assign append
append returns the resulting slice header and may allocate a new backing array.
Shadowing a result or error
A short declaration in an inner block can create a new variable and leave the outer value unchanged.
Broken and corrected examples#
Each pair names the failing case that the language fixture must catch.
Depending on map iteration order
Failure: Map iteration order is unspecified, so output can vary across runs and cannot implement lexicographic tie-breaking.
Regression case: Keys(map[string]int{"beta": 1, "alpha": 2, "gamma": 1}) must equal []string{"alpha", "beta", "gamma"}.
func Keys(counts map[string]int) []string {
keys := []string{}
for key := range counts {
keys = append(keys, key)
}
return keys // order is unspecified
}func Keys(counts map[string]int) []string {
keys := make([]string, 0, len(counts))
for key := range counts {
keys = append(keys, key)
}
slices.Sort(keys)
return keys
}Treating byte length as character count
Failure: Reversing UTF-8 bytes independently can split a multibyte encoding and produce invalid text. The fixed contract reverses Unicode code points, not grapheme clusters.
Regression case: Reverse("aé") must equal "éa", and utf8.ValidString(result) must be true.
func Reverse(text string) string {
bytes := []byte(text)
for left, right := 0, len(bytes)-1; left < right; left, right = left+1, right-1 {
bytes[left], bytes[right] = bytes[right], bytes[left]
}
return string(bytes)
}func Reverse(text string) string {
runes := []rune(text)
for left, right := 0, len(runes)-1; left < right; left, right = left+1, right-1 {
runes[left], runes[right] = runes[right], runes[left]
}
return string(runes)
}Returning an aliased mutable path
Failure: The stored slice shares the path's backing array, so later backtracking can overwrite an earlier answer.
Regression case: Record path []int{1}, mutate path[0] to 9, and require the recorded result to remain []int{1}.
func record(results *[][]int, path []int) {
*results = append(*results, path)
}func record(results *[][]int, path []int) {
snapshot := append([]int(nil), path...)
*results = append(*results, snapshot)
}Calling heap interface methods directly
Failure: IntHeap.Push and IntHeap.Pop only implement the backing-slice hooks expected by container/heap. Calling them directly does not initialize or restore the heap invariant.
Regression case: Starting with 4, 1, 3 and pushing 2, the first heap.Pop result must be 1, followed by 2, 3, and 4.
h := &IntHeap{4, 1, 3}
h.Push(2)
smallest := h.Pop().(int) // not a heap operation; may return the wrong itemh := &IntHeap{4, 1, 3}
heap.Init(h)
heap.Push(h, 2)
smallest := heap.Pop(h).(int)Language transfer exercises#
Use these to check runtime judgment before another algorithm repetition. Open the answer check only after committing to a result.
- predict
Predict slice aliasing
base := []int{1,2,3,4}; view := base[1:3]; view[0] = 9. Predict base and explain whether append(view, 8) is guaranteed to allocate.
Answer check
base becomes [1,9,3,4]. append is not guaranteed to allocate because view has remaining capacity in this construction; copy explicitly when isolation matters.
- trace
Trace comma-ok
A map contains counts["seen"] = 0 while "missing" is absent. Trace counts[key] and the comma-ok result for both keys.
Answer check
Both value lookups return 0, but ok is true for "seen" and false for "missing". The boolean preserves the presence distinction.
- repair
Repair a Unicode reversal
A byte-swap reversal corrupts "aé". Repair it for a code-point contract and state the remaining limitation.
Answer check
Convert to []rune, swap runes, then convert back to string. This reverses code points but does not keep multi-code-point grapheme clusters intact or normalize text.
- choose
Choose the right standard-library tool
Choose among a head-index slice, map[T]struct{}, slices.SortFunc, sort.Search, and container/heap for FIFO traversal, membership, multi-key ordering, a monotonic boundary, and repeated minimum removal.
Answer check
Use the head-index slice, set-shaped map, slices.SortFunc, sort.Search, and container/heap respectively, after confirming their input and tie contracts.
- transfer
Unlabeled transfer
Maintain the three smallest delivery deadlines, breaking equal deadlines by arrival sequence. State the heap element, Less rule, capacity behavior, and integer type.
Answer check
Store deadline plus monotonically increasing sequence and payload. Use a size-three max-oriented heap so the worst retained item is removable, compare deadline then sequence consistently, and use int64 for externally supplied deadline values when their range requires it.