Go Idioms — DSA & Interview Snippets
Short, runnable patterns. No single-letter names in examples.
Slices vs arrays and make
Array: fixed length, value type (copied on assignment). Slice: descriptor (ptr, len, cap) over a backing array. Preallocate capacity with make when size is known to limit reallocations.
package main
import "fmt"
func main() {
fixed := [3]int{1, 2, 3}
buffer := make([]int, 0, 8)
buffer = append(buffer, 1, 2, 3, 4)
fmt.Println(fixed, len(buffer), cap(buffer))
}append pitfalls
append may allocate a new backing array; holding references to old slice headers after heavy append can show stale views.
package main
import "fmt"
func main() {
first := []int{1, 2}
second := append(first, 3)
first[0] = 9
fmt.Println(first, second)
}Shared backing: mutating indices still visible through aliases until append grows past cap.
Map iteration order is undefined
package main
import "fmt"
func main() {
scores := map[string]int{"ada": 10, "bob": 20}
for name := range scores {
fmt.Println(name)
}
}Do not rely on order; sort keys explicitly if order matters.
defer gotchas
defer runs LIFO at function return; arguments evaluated immediately (not at defer time).
package main
import "fmt"
func main() {
value := 1
defer fmt.Println(value)
value = 2
fmt.Println("done")
}Loop + defer inside the loop defers until function exit (often wrong for cleanup—use a local function or avoid defer in tight loops).
Error wrapping (fmt.Errorf + %w)
package main
import (
"errors"
"fmt"
)
func main() {
inner := errors.New("root cause")
wrapped := fmt.Errorf("context: %w", inner)
fmt.Println(errors.Is(wrapped, inner))
}Use %w for unwrap / errors.Is / errors.As chains.
Mutex vs channels (rule of thumb)
Protect shared memory with sync.Mutex (or RWMutex). Channels orchestrate goroutines and pass ownership—do not replace every mutex with a channel “for idiomatic Go.”
package main
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
total := 0
var wait sync.WaitGroup
for offset := 0; offset < 3; offset++ {
wait.Add(1)
go func(delta int) {
defer wait.Done()
mutex.Lock()
total += delta
mutex.Unlock()
}(offset)
}
wait.Wait()
fmt.Println(total)
}container/heap interface checklist
Implement heap.Interface: Len, Less, Swap, Push(x any), Pop() any. After mutations, call heap.Fix, heap.Push, or heap.Pop—not only raw slice edits.
package main
import (
"container/heap"
"fmt"
)
type IntHeap []int
func (receiver IntHeap) Len() int { return len(receiver) }
func (receiver IntHeap) Less(left, right int) bool { return receiver[left] < receiver[right] }
func (receiver IntHeap) Swap(left, right int) { receiver[left], receiver[right] = receiver[right], receiver[left] }
func (receiver *IntHeap) Push(value any) { *receiver = append(*receiver, value.(int)) }
func (receiver *IntHeap) Pop() any {
old := *receiver
last := old[len(old)-1]
*receiver = old[:len(old)-1]
return last
}
func main() {
values := &IntHeap{3, 1, 2}
heap.Init(values)
heap.Push(values, 0)
fmt.Println(heap.Pop(values))
}strings.Builder
package main
import (
"fmt"
"strings"
)
func main() {
var builder strings.Builder
parts := []string{"a", "b", "c"}
for _, part := range parts {
builder.WriteString(part)
}
fmt.Println(builder.String())
}Avoid repeated string concatenation in hot loops.
Runes vs bytes (Unicode)
Bytes: UTF-8 raw; runes: Unicode code points. Indexing a string byte-wise can split multibyte characters.
package main
import "fmt"
func main() {
text := "éclair"
fmt.Println(len(text), len([]rune(text)))
for _, runeValue := range text {
fmt.Printf("%U ", runeValue)
}
}sort.Slice
package main
import (
"fmt"
"sort"
)
func main() {
pairs := [][]int{{2, 9}, {1, 5}, {3, 4}}
sort.Slice(pairs, func(left, right int) bool {
return pairs[left][0] < pairs[right][0]
})
fmt.Println(pairs)
}Idiomatic index loops
package main
import "fmt"
func main() {
items := []string{"x", "y"}
for index := range items {
fmt.Println(index, items[index])
}
for index, value := range items {
fmt.Println(index, value)
}
}See also: Data structure operations.
Last updated on
Spotted something unclear or wrong on this page?