3 Core Engineering Pillars
Designing a high-throughput LRU cache in Go requires balancing strict $O(1)$ algorithmic guarantees against Go runtime characteristics like heap escape analysis and lock contention.
type node[K comparable, V any] struct {
key K; val V
prev, next *node[K, V]
}
removeNode() and addToHead() never need conditional checks for list boundary edges.
interface{}, forcing heap boxing allocations on every Get() and Put(). Go 1.26 type parameters [K comparable, V any] allow monomorphized scalar values to live inside nodes without boxing.
type LRUCache[K comparable, V any] struct {
capacity int
items map[K]*node[K, V]
}
[int64, int64]), Go's garbage collector completely skips scanning cache nodes!
Get() MUST promote the accessed node to the MRU head! That is an in-place mutation of prev and next pointers.
mu.RLock() for Get() causes two concurrent readers to simultaneously rewrite list pointers, corrupting memory and panicking under -race.
mu.Lock() or partition cache across 16 independent mutex-striped shards to keep throughput high.
Interactive LRU Cache Policies & Eviction Workload
Step through an active LRU cache workload to visualize doubly-linked list node splicing, hash map pointer updates, and eviction under capacity limits.
func (c *LRUCache[K, V]) Get(k K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if node, ok := c.items[k]; ok {
c.moveToHead(node) // MRU
return node.val, true
}
var zero V
return zero, false
}
func (c *LRUCache[K, V]) moveToHead(n *node[K, V]) {
c.removeNode(n)
c.addToHead(n)
}
func (c *LRUCache[K, V]) Put(k K, v V) {
c.mu.Lock()
defer c.mu.Unlock()
if node, ok := c.items[k]; ok {
node.val = v
c.moveToHead(node)
return
}
if len(c.items) >= c.capacity {
tail := c.removeTail() // Evict LRU
delete(c.items, tail.key)
}
// Insert new MRU node
}
3 Fatal Production LRU Pitfalls & Post-Mortems
Real incidents from high-traffic production Go systems where improper cache design triggered memory corruption, lock starvation, and silent leaks.
sync.RWMutex.RLock() for Get(). But LRU requires moving accessed items to the head — a pointer write that corrupts the linked list under concurrent load.
Lock() or decouple reads into a channel-buffered batch updater.
[]byte or *User), returning it directly from Get() allows callers to mutate internal cached state without holding the cache lock.
copy() for byte slices) before returning.
prev and next pointers to nil can keep referenced neighbors pinned in memory if any external code holds a node reference.
node.prev = nil and node.next = nil upon eviction so GC can reclaim unreferenced memory immediately.
Sharded Striped LRU vs Single RWMutex Lock
To resolve the single mutex bottleneck, high-performance Go caches partition the keyspace into $N$ independent cache shards (typically 16 or 32 stripes). Each shard has its own mutex and doubly-linked list.
| Cache Architecture | Throughput (ns/op) | Ops / Second | Allocs/op | Contention Characteristic |
|---|---|---|---|---|
| Naive Single sync.Mutex LRU | 382 ns/op | 2.62 M ops/s | 0 B / op | High Contention Bottleneck |
| Naive Single sync.RWMutex (Get Lock) | 394 ns/op | 2.54 M ops/s | 0 B / op | Worst Case: RWMutex Overhead |
| Sharded 16-Shard LRU (16 Mutexes) | 42 ns/op | 23.81 M ops/s | 0 B / op | Near-Linear 9.1x Speedup |
| sync.Map + Free-List Cache (Lockless) | 28 ns/op | 35.71 M ops/s | 0 B / op | Zero Mutex on Read Path |
Production Alert: p99 Latency Collapse Under Black Friday Load
Scenario: At 00:01 on Black Friday, your payment gateway encounters 200,000 requests/sec. Profiling reveals sync.RWMutex.Lock() in your centralized LRU cache is consuming 84% of CPU time, with 32 worker goroutines waiting on a single cache mutex lock. What architectural refactor permanently solves this?
Go Coding Interview Problem Catalog
Pick a category below to practice standard Go coding questions with runnable verified code:
1. Implement a singly linked list
Build a singly linked list supporting insertion at the head and tail, deletion by value, and forward traversal for printing.
package main
import "fmt"
type Node struct {
val int
next *Node
}
type List struct {
head *Node
// tracking tail separately makes InsertTail O(1); without it we'd have
// to walk the whole list on every append
tail *Node
}
func (l *List) InsertHead(v int) {
n := &Node{val: v, next: l.head}
l.head = n
// first node ever inserted is both head and tail
if l.tail == nil {
l.tail = n
}
}
func (l *List) InsertTail(v int) {
n := &Node{val: v}
// empty list: head and tail both need to point at the new node,
// there's no existing tail to link from
if l.tail == nil {
l.head = n
l.tail = n
return
}
l.tail.next = n
l.tail = n
}
func (l *List) Delete(v int) bool {
// prev starts nil so deleting the head is just "no prev to relink"
var prev *Node
cur := l.head
for cur != nil {
if cur.val == v {
if prev == nil {
// deleting the head: no predecessor to relink, just advance head
l.head = cur.next
} else {
prev.next = cur.next
}
// deleting the tail: tail must move back to the predecessor,
// otherwise tail would dangle on a node no longer reachable from head
if cur == l.tail {
l.tail = prev
}
return true
}
prev = cur
cur = cur.next
}
return false
}
func (l *List) String() string {
s := ""
for cur := l.head; cur != nil; cur = cur.next {
if s != "" {
s += " -> "
}
s += fmt.Sprintf("%d", cur.val)
}
return s
}
func main() {
l := &List{}
l.InsertTail(1)
l.InsertTail(2)
l.InsertTail(3)
l.InsertHead(0)
fmt.Println("After inserts:", l.String())
l.Delete(2)
fmt.Println("After deleting 2:", l.String())
l.Delete(0)
fmt.Println("After deleting head 0:", l.String())
l.InsertTail(4)
fmt.Println("After inserting tail 4:", l.String())
}
2. Reverse a singly linked list
Reverse the direction of every next pointer in a singly linked list using the standard three-pointer iterative technique, in a single O(n) pass.
package main
import "fmt"
type Node struct {
val int
next *Node
}
func build(vals []int) *Node {
// keep a tail pointer so appending stays O(1) instead of walking the
// whole list to find the end on every insert
var head, tail *Node
for _, v := range vals {
n := &Node{val: v}
if head == nil {
head = n
tail = n
} else {
tail.next = n
tail = n
}
}
return head
}
func reverse(head *Node) *Node {
// prev trails cur by one step; once cur passes a node, prev is the
// only link back to it, so nothing is lost even though we're about
// to overwrite that node's forward pointer
var prev *Node
cur := head
for cur != nil {
// stash next before we clobber cur.next, or we'd lose the rest of the list
next := cur.next
cur.next = prev
prev = cur
cur = next
}
// when cur becomes nil, prev is sitting on the old tail, i.e. the new head
return prev
}
func print(head *Node) string {
s := ""
for cur := head; cur != nil; cur = cur.next {
if s != "" {
s += " -> "
}
s += fmt.Sprintf("%d", cur.val)
}
return s
}
func main() {
head := build([]int{1, 2, 3, 4, 5})
fmt.Println("Original:", print(head))
reversed := reverse(head)
fmt.Println("Reversed:", print(reversed))
// a one-node list is its own reverse; exercises the loop running exactly once
single := build([]int{42})
fmt.Println("Single node reversed:", print(reverse(single)))
}
3. Detect a cycle in a linked list
Use Floyd's tortoise-and-hare algorithm — a slow pointer and a fast pointer — to detect whether a linked list contains a cycle, without extra memory.
package main
import "fmt"
type Node struct {
val int
next *Node
}
func hasCycle(head *Node) bool {
// Floyd's tortoise and hare: fast moves twice as fast as slow. If there's
// a cycle, fast eventually laps slow inside the loop and they collide; if
// the list is acyclic, fast simply runs off the end first
slow, fast := head, head
// checking fast and fast.next (not just fast) lets fast.next.next below
// run safely without a nil dereference
for fast != nil && fast.next != nil {
slow = slow.next
fast = fast.next.next
if slow == fast {
return true
}
}
return false
}
func main() {
// Build a clean list: 1 -> 2 -> 3 -> 4 -> nil
n4 := &Node{val: 4}
n3 := &Node{val: 3, next: n4}
n2 := &Node{val: 2, next: n3}
n1 := &Node{val: 1, next: n2}
fmt.Println("List without cycle, hasCycle:", hasCycle(n1))
// Introduce a cycle: n4 -> n2 (loop back)
n4.next = n2
fmt.Println("List with cycle (4 -> 2), hasCycle:", hasCycle(n1))
// Single node pointing to itself: a cycle of length 1, so slow and fast
// collide on the very first step of the loop
self := &Node{val: 99}
self.next = self
fmt.Println("Single self-looping node, hasCycle:", hasCycle(self))
// Empty list: head is nil, so the loop condition fails immediately
// rather than needing a special-case check
fmt.Println("Empty list, hasCycle:", hasCycle(nil))
}
4. Find the middle node of a linked list in one pass
Find the middle node of a singly linked list in a single traversal using two pointers, one advancing twice as fast as the other.
package main
import "fmt"
type Node struct {
val int
next *Node
}
func build(vals []int) *Node {
// tail pointer avoids re-walking the list to find the append point
var head, tail *Node
for _, v := range vals {
n := &Node{val: v}
if head == nil {
head = n
tail = n
} else {
tail.next = n
tail = n
}
}
return head
}
func middle(head *Node) *Node {
// fast covers two nodes for every one slow covers, so when fast runs
// out of list, slow has covered exactly half the distance -- one pass,
// no need to count the length first
slow, fast := head, head
for fast != nil && fast.next != nil {
slow = slow.next
fast = fast.next.next
}
// for even-length lists fast runs out one step early, leaving slow on
// the second (upper) of the two middle nodes
return slow
}
func main() {
odd := build([]int{1, 2, 3, 4, 5})
fmt.Println("Odd length [1..5], middle:", middle(odd).val)
even := build([]int{1, 2, 3, 4, 5, 6})
fmt.Println("Even length [1..6], middle:", middle(even).val)
single := build([]int{7})
fmt.Println("Single node [7], middle:", middle(single).val)
two := build([]int{10, 20})
fmt.Println("Two nodes [10,20], middle:", middle(two).val)
}
5. Implement a stack (push, pop, peek, isEmpty)
Implement a LIFO stack backed by a Go slice, with Push, Pop, Peek, and IsEmpty operations, returning an error when popping or peeking an empty stack.
package main
import (
"errors"
"fmt"
)
// a slice already grows from one end in amortized O(1), which is exactly
// the shape a stack needs -- no separate node/pointer bookkeeping required
type Stack struct {
items []int
}
func (s *Stack) Push(v int) {
// the end of the slice is the top of the stack
s.items = append(s.items, v)
}
func (s *Stack) Pop() (int, error) {
// return an error instead of panicking so callers can decide how to
// handle "nothing to pop" rather than crashing the program
if s.IsEmpty() {
return 0, errors.New("stack is empty")
}
n := len(s.items) - 1
v := s.items[n]
// re-slicing to drop the last element reuses the backing array --
// no allocation, the old last element just becomes unreachable
s.items = s.items[:n]
return v, nil
}
func (s *Stack) Peek() (int, error) {
if s.IsEmpty() {
return 0, errors.New("stack is empty")
}
return s.items[len(s.items)-1], nil
}
func (s *Stack) IsEmpty() bool {
return len(s.items) == 0
}
func main() {
s := &Stack{}
s.Push(1)
s.Push(2)
s.Push(3)
top, _ := s.Peek()
fmt.Println("Peek:", top)
v, _ := s.Pop()
fmt.Println("Pop:", v)
v, _ = s.Pop()
fmt.Println("Pop:", v)
fmt.Println("IsEmpty:", s.IsEmpty())
v, _ = s.Pop()
fmt.Println("Pop:", v)
fmt.Println("IsEmpty:", s.IsEmpty())
_, err := s.Pop()
fmt.Println("Pop on empty, error:", err)
}
6. Implement a queue (enqueue, dequeue, front, isEmpty)
Implement a FIFO queue backed by a Go slice, with Enqueue, Dequeue, Front, and IsEmpty operations, returning an error when dequeuing from an empty queue.
package main
import (
"errors"
"fmt"
)
type Queue struct {
items []int
}
func (q *Queue) Enqueue(v int) {
// new items always join at the back; combined with Dequeue reading
// from index 0, this is what gives FIFO order (vs. a stack's LIFO)
q.items = append(q.items, v)
}
func (q *Queue) Dequeue() (int, error) {
if q.IsEmpty() {
return 0, errors.New("queue is empty")
}
v := q.items[0]
// re-slicing off the front is simple and correct, but note it just
// advances the slice's start pointer -- the backing array still holds
// the dequeued elements until the whole array is garbage collected, so
// a long-lived, high-churn queue would want a ring buffer instead
// (see the circular buffer example)
q.items = q.items[1:]
return v, nil
}
func (q *Queue) Front() (int, error) {
if q.IsEmpty() {
return 0, errors.New("queue is empty")
}
return q.items[0], nil
}
func (q *Queue) IsEmpty() bool {
return len(q.items) == 0
}
func main() {
q := &Queue{}
q.Enqueue(1)
q.Enqueue(2)
q.Enqueue(3)
front, _ := q.Front()
fmt.Println("Front:", front)
v, _ := q.Dequeue()
fmt.Println("Dequeue:", v)
v, _ = q.Dequeue()
fmt.Println("Dequeue:", v)
fmt.Println("IsEmpty:", q.IsEmpty())
v, _ = q.Dequeue()
fmt.Println("Dequeue:", v)
fmt.Println("IsEmpty:", q.IsEmpty())
_, err := q.Dequeue()
fmt.Println("Dequeue on empty, error:", err)
}
7. Implement a stack using two queues
Implement a LIFO stack using only two FIFO queues as the underlying storage, making Push do the extra work of re-ordering elements so Pop always removes the most recently pushed item.
package main
import "fmt"
// StackTwoQueues implements a LIFO stack using two FIFO queues.
type StackTwoQueues struct {
q1 []int
q2 []int
}
// dequeue pops the front element by reslicing past index 0 (no shifting
// or copying of the remaining elements).
func dequeue(q *[]int) int {
v := (*q)[0]
*q = (*q)[1:]
return v
}
// Push makes the new element the front of q1 by rotating existing
// elements through q2, keeping the most recently pushed item first.
func (s *StackTwoQueues) Push(v int) {
s.q2 = append(s.q2, v)
for len(s.q1) > 0 {
s.q2 = append(s.q2, dequeue(&s.q1))
}
s.q1, s.q2 = s.q2, s.q1
}
// Pop is O(1) because all the reordering work happened in Push: q1 is
// always kept with the most-recently-pushed item at the front.
func (s *StackTwoQueues) Pop() int {
return dequeue(&s.q1)
}
func (s *StackTwoQueues) IsEmpty() bool {
return len(s.q1) == 0
}
func main() {
s := &StackTwoQueues{}
s.Push(1)
s.Push(2)
s.Push(3)
fmt.Println("Pop:", s.Pop())
fmt.Println("Pop:", s.Pop())
s.Push(4)
fmt.Println("Pop:", s.Pop())
fmt.Println("Pop:", s.Pop())
fmt.Println("IsEmpty:", s.IsEmpty())
}
8. Implement a queue using two stacks
Implement a FIFO queue using only two LIFO stacks: an input stack for enqueuing and an output stack that lazily reverses elements so Dequeue always removes the oldest item.
package main
import "fmt"
// QueueTwoStacks implements a FIFO queue using two LIFO stacks.
type QueueTwoStacks struct {
in []int
out []int
}
// Enqueue is always O(1): new items just pile onto in, in reverse-of-
// dequeue order, and get flipped into out lazily by shift.
func (q *QueueTwoStacks) Enqueue(v int) {
q.in = append(q.in, v)
}
// shift moves all elements from in to out (reversing order) only when
// out is empty, so dequeue returns them in original insertion order.
func (q *QueueTwoStacks) shift() {
if len(q.out) == 0 {
for len(q.in) > 0 {
n := len(q.in) - 1
q.out = append(q.out, q.in[n])
q.in = q.in[:n]
}
}
}
// Dequeue only pays the O(n) shift cost once per batch of enqueues;
// each element is moved from in to out exactly once over its lifetime,
// so the amortized cost per operation is still O(1).
func (q *QueueTwoStacks) Dequeue() int {
q.shift()
n := len(q.out) - 1
v := q.out[n]
q.out = q.out[:n]
return v
}
func (q *QueueTwoStacks) IsEmpty() bool {
return len(q.in) == 0 && len(q.out) == 0
}
func main() {
q := &QueueTwoStacks{}
q.Enqueue(1)
q.Enqueue(2)
q.Enqueue(3)
fmt.Println("Dequeue:", q.Dequeue())
fmt.Println("Dequeue:", q.Dequeue())
q.Enqueue(4)
fmt.Println("Dequeue:", q.Dequeue())
fmt.Println("Dequeue:", q.Dequeue())
fmt.Println("IsEmpty:", q.IsEmpty())
}
9. Implement a doubly linked list
Build a doubly linked list with PushFront, PushBack, and O(1) Remove of an arbitrary node, supporting traversal in both directions via prev and next pointers.
package main
import "fmt"
type DNode struct {
val int
prev *DNode
next *DNode
}
type DoublyList struct {
head *DNode
tail *DNode
}
// PushBack wires n.prev up front (from the current tail) so the two
// pointer updates below only ever have to look one direction: forward
// from the old tail, or straight into head/tail if the list was empty.
func (l *DoublyList) PushBack(v int) *DNode {
n := &DNode{val: v, prev: l.tail}
if l.tail != nil {
l.tail.next = n
} else {
// list was empty: n is both the first and last node
l.head = n
}
l.tail = n
return n
}
// PushFront is PushBack's mirror image: n.next is set from the old
// head instead of n.prev from the old tail.
func (l *DoublyList) PushFront(v int) *DNode {
n := &DNode{val: v, next: l.head}
if l.head != nil {
l.head.prev = n
} else {
// list was empty: n is both the first and last node
l.tail = n
}
l.head = n
return n
}
// Remove is O(1) given a node pointer, unlike a singly linked list where
// you'd need to walk from head to find the predecessor first. Each side
// is patched independently: if n has no prev/next, it was the head/tail,
// so the list's head/tail pointer moves instead of a neighbor's link.
func (l *DoublyList) Remove(n *DNode) {
if n.prev != nil {
n.prev.next = n.next
} else {
l.head = n.next
}
if n.next != nil {
n.next.prev = n.prev
} else {
l.tail = n.prev
}
}
func (l *DoublyList) Forward() string {
s := ""
for cur := l.head; cur != nil; cur = cur.next {
if s != "" {
s += " <-> "
}
s += fmt.Sprintf("%d", cur.val)
}
return s
}
// Backward walks via prev pointers instead of next - the whole reason
// to pay for a second pointer per node is to support this in O(n) instead
// of needing to reverse or re-walk from head each time.
func (l *DoublyList) Backward() string {
s := ""
for cur := l.tail; cur != nil; cur = cur.prev {
if s != "" {
s += " <-> "
}
s += fmt.Sprintf("%d", cur.val)
}
return s
}
func main() {
l := &DoublyList{}
l.PushBack(2)
l.PushBack(3)
mid := l.PushFront(1)
l.PushBack(4)
fmt.Println("Forward: ", l.Forward())
fmt.Println("Backward:", l.Backward())
l.Remove(mid)
fmt.Println("After removing 1:")
fmt.Println("Forward: ", l.Forward())
fmt.Println("Backward:", l.Backward())
}
10. Implement a binary search tree
Implement a binary search tree supporting Insert, Search, and an in-order traversal that returns the stored values in sorted order.
package main
import "fmt"
type TreeNode struct {
val int
left *TreeNode
right *TreeNode
}
type BST struct {
root *TreeNode
}
func (t *BST) Insert(v int) {
t.root = insertNode(t.root, v)
}
// insertNode returns the (possibly new) subtree root so each caller on
// the way back up can rewire its left/right pointer - this is what lets
// a brand-new leaf get attached without the caller needing a nil check
// of its own. Equal values are silently dropped (no else branch), so the
// tree only ever holds one node per distinct key.
func insertNode(n *TreeNode, v int) *TreeNode {
if n == nil {
return &TreeNode{val: v}
}
if v < n.val {
n.left = insertNode(n.left, v)
} else if v > n.val {
n.right = insertNode(n.right, v)
}
return n
}
// Search follows the same left/right comparison as insertNode but can
// be a plain loop instead of recursion: it never needs to rewire a
// pointer on the way back up, only to answer yes/no.
func (t *BST) Search(v int) bool {
cur := t.root
for cur != nil {
if v == cur.val {
return true
} else if v < cur.val {
cur = cur.left
} else {
cur = cur.right
}
}
return false
}
// InOrder visits left-subtree, node, right-subtree in that order, which
// for a BST always produces keys in sorted order - that's the defining
// property this traversal is used to demonstrate.
func (t *BST) InOrder() []int {
var result []int
// declared separately so the closure can reference walk recursively
var walk func(n *TreeNode)
walk = func(n *TreeNode) {
if n == nil {
return
}
walk(n.left)
result = append(result, n.val)
walk(n.right)
}
walk(t.root)
return result
}
func main() {
t := &BST{}
for _, v := range []int{8, 3, 10, 1, 6, 14, 4, 7, 13} {
t.Insert(v)
}
fmt.Println("In-order traversal:", t.InOrder())
fmt.Println("Search 6:", t.Search(6))
fmt.Println("Search 9:", t.Search(9))
fmt.Println("Search 13:", t.Search(13))
}
11. Implement a min-heap / priority queue
Implement a min-heap priority queue for ints by satisfying container/heap.Interface, then use heap.Push and heap.Pop to always retrieve the smallest remaining element.
package main
import (
"container/heap"
"fmt"
)
// MinHeap is a min-heap of ints implementing container/heap.Interface.
type MinHeap []int
func (h MinHeap) Len() int { return len(h) }
// Less is the only line that decides min-heap vs max-heap: reporting the
// smaller element as "less" is what keeps the minimum at index 0.
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] }
// Push/Pop only manage the backing slice's length; they are never called
// directly. Package heap wraps them: heap.Push appends here then sifts
// the new element up, and heap.Pop swaps the root to the last slot and
// sifts down before calling this Pop - which is why it's always safe to
// take the element off the end rather than off the front.
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
old := *h
n := len(old)
v := old[n-1]
*h = old[:n-1]
return v
}
func main() {
h := &MinHeap{}
heap.Init(h)
for _, v := range []int{5, 2, 9, 1, 7, 3} {
heap.Push(h, v)
}
fmt.Println("Heap size after pushes:", h.Len())
fmt.Print("Popped in order: ")
for h.Len() > 0 {
fmt.Print(heap.Pop(h), " ")
}
fmt.Println()
heap.Push(h, 42)
heap.Push(h, 10)
fmt.Println("Min after two pushes:", (*h)[0])
}
12. Implement an LRU cache
Implement an LRU cache with O(1) Get and Put by combining a map for key lookup with a doubly linked list that tracks recency, evicting the least-recently-used entry when the cache is full.
package main
import "fmt"
type lruNode struct {
key, val int
prev, next *lruNode
}
// LRUCache gives O(1) Get/Put using a map for lookup plus a doubly
// linked list to track recency: front is most-recently-used, back is
// least-recently-used and the first to be evicted.
type LRUCache struct {
capacity int
items map[int]*lruNode
head, tail *lruNode // head = most recent, tail = least recent
}
func NewLRUCache(capacity int) *LRUCache {
return &LRUCache{capacity: capacity, items: make(map[int]*lruNode)}
}
// remove unlinks n from the list in O(1) - no need to search for it,
// since the map already gave us the node pointer directly.
func (c *LRUCache) remove(n *lruNode) {
if n.prev != nil {
n.prev.next = n.next
} else {
c.head = n.next
}
if n.next != nil {
n.next.prev = n.prev
} else {
c.tail = n.prev
}
n.prev, n.next = nil, nil
}
// pushFront marks n as the most-recently-used entry by making it the
// new head. remove+pushFront together are how both Get and Put "touch"
// a key without duplicating the splice logic.
func (c *LRUCache) pushFront(n *lruNode) {
n.next = c.head
n.prev = nil
if c.head != nil {
c.head.prev = n
}
c.head = n
if c.tail == nil {
c.tail = n
}
}
// Get is itself a "write": a successful read counts as a use, so the
// node has to move to the front or it would look stale next eviction.
func (c *LRUCache) Get(key int) (int, bool) {
n, ok := c.items[key]
if !ok {
return 0, false
}
c.remove(n)
c.pushFront(n)
return n.val, true
}
func (c *LRUCache) Put(key, val int) {
// key already present: overwrite in place and just re-touch it -
// no eviction check needed since the map size isn't growing.
if n, ok := c.items[key]; ok {
n.val = val
c.remove(n)
c.pushFront(n)
return
}
// only a genuinely new key can push us over capacity, and c.tail is
// by construction the least-recently-used node - the one to evict.
if len(c.items) >= c.capacity {
lru := c.tail
c.remove(lru)
delete(c.items, lru.key)
}
n := &lruNode{key: key, val: val}
c.items[key] = n
c.pushFront(n)
}
func main() {
c := NewLRUCache(2)
c.Put(1, 100)
c.Put(2, 200)
v, ok := c.Get(1)
fmt.Println("Get(1):", v, ok)
c.Put(3, 300) // evicts key 2 (least recently used)
_, ok = c.Get(2)
fmt.Println("Get(2) after eviction:", ok)
v, ok = c.Get(3)
fmt.Println("Get(3):", v, ok)
c.Put(4, 400) // evicts key 1 (least recently used now)
_, ok = c.Get(1)
fmt.Println("Get(1) after eviction:", ok)
v, ok = c.Get(4)
fmt.Println("Get(4):", v, ok)
}
13. Reverse a doubly linked list
Given a doubly linked list, reverse it in place by swapping each node's prev/next pointers and swapping the list's head/tail references.
package main
import "fmt"
type dnode struct {
val int
prev, next *dnode
}
type dlist struct {
head, tail *dnode
}
func (l *dlist) pushBack(v int) {
n := &dnode{val: v}
// empty list is a special case: head and tail both point at the sole node
if l.head == nil {
l.head, l.tail = n, n
return
}
n.prev = l.tail
l.tail.next = n
l.tail = n
}
// a doubly linked list reverses in O(n) with no new nodes: every node already
// holds both neighbors, so reversing just means swapping each node's prev/next,
// then swapping which end of the list is called head vs tail.
func (l *dlist) reverse() {
cur := l.head
// flip the list's own head/tail pointers first, before touching individual nodes
l.head, l.tail = l.tail, l.head
for cur != nil {
// swap this node's own prev/next so traversal direction inverts locally
cur.prev, cur.next = cur.next, cur.prev
cur = cur.prev // after the swap, .prev holds the old .next, so this still walks forward
}
}
func (l *dlist) forward() []int {
var out []int
for n := l.head; n != nil; n = n.next {
out = append(out, n.val)
}
return out
}
func (l *dlist) backward() []int {
var out []int
for n := l.tail; n != nil; n = n.prev {
out = append(out, n.val)
}
return out
}
func main() {
l := &dlist{}
for _, v := range []int{1, 2, 3, 4, 5} {
l.pushBack(v)
}
fmt.Println("before:", l.forward())
l.reverse()
fmt.Println("after: ", l.forward())
fmt.Println("backward from tail:", l.backward())
}
14. Merge two sorted linked lists into one sorted linked list
Given two singly linked lists whose values are already sorted ascending, splice them together into a single sorted list without allocating new nodes.
package main
import "fmt"
type node struct {
val int
next *node
}
// a dummy head node sidesteps the need to special-case "which list starts the
// result": we always have a node to hang tail.next off of, even before the
// first real element is chosen.
func mergeSorted(a, b *node) *node {
dummy := &node{}
tail := dummy
for a != nil && b != nil {
// splice the smaller node in directly and reuse it, rather than allocating
// a new node and copying the value, so the merge is O(1) extra space
if a.val <= b.val {
tail.next = a
a = a.next
} else {
tail.next = b
b = b.next
}
tail = tail.next
}
// one list is exhausted, but the other is still sorted, so its remaining
// tail can be attached in one shot instead of walking it node by node
if a != nil {
tail.next = a
} else {
tail.next = b
}
return dummy.next // skip past the dummy to return the real head
}
func build(vals []int) *node {
dummy := &node{}
tail := dummy
for _, v := range vals {
tail.next = &node{val: v}
tail = tail.next
}
return dummy.next
}
func toSlice(n *node) []int {
var out []int
for ; n != nil; n = n.next {
out = append(out, n.val)
}
return out
}
func main() {
a := build([]int{1, 3, 5, 7})
b := build([]int{2, 4, 6, 8, 10})
fmt.Println("list a:", toSlice(a))
fmt.Println("list b:", toSlice(b))
merged := mergeSorted(a, b)
fmt.Println("merged: ", toSlice(merged))
}
15. Remove the Nth node from the end of a linked list in one pass
Delete the node that is n positions from the end of a singly linked list using a fast/slow two-pointer technique, without first computing the list's length.
package main
import "fmt"
type node struct {
val int
next *node
}
func build(vals []int) *node {
dummy := &node{}
tail := dummy
for _, v := range vals {
tail.next = &node{val: v}
tail = tail.next
}
return dummy.next
}
func toSlice(n *node) []int {
var out []int
for ; n != nil; n = n.next {
out = append(out, n.val)
}
return out
}
// removeNthFromEnd deletes the nth node counting from the end (1-indexed)
// using two pointers separated by n steps, in a single pass.
func removeNthFromEnd(head *node, n int) *node {
// dummy precedes head so slow can end up pointing at the node *before* the
// one to remove even when that's the real head itself (n == length)
dummy := &node{next: head}
fast, slow := dummy, dummy
// walk fast n steps ahead first, so the gap between fast and slow is fixed at n
for i := 0; i < n; i++ {
fast = fast.next
}
// advance both until fast falls off the end; slow is then exactly n nodes
// behind, i.e. sitting right before the node to delete
for fast.next != nil {
fast = fast.next
slow = slow.next
}
slow.next = slow.next.next // unlink the target node
return dummy.next
}
func main() {
head := build([]int{1, 2, 3, 4, 5})
fmt.Println("original:", toSlice(head))
head = removeNthFromEnd(head, 2)
fmt.Println("remove 2nd from end:", toSlice(head))
head = removeNthFromEnd(head, 1)
fmt.Println("remove last:", toSlice(head))
head = removeNthFromEnd(head, 3)
fmt.Println("remove head (3rd from end of 3):", toSlice(head))
}
16. Implement a trie (prefix tree)
Build a trie with Insert, Search, and StartsWith operations, using a fixed 26-entry child array per node for lowercase letters.
package main
import "fmt"
// a fixed-size array indexed by letter is a common trie shortcut when the
// alphabet is known and small (lowercase a-z); a map[rune]*trieNode would be
// needed for arbitrary characters, at the cost of extra allocation per node
type trieNode struct {
children [26]*trieNode
isEnd bool // marks "a real word ends here", distinct from "a prefix passes through here"
}
type trie struct {
root *trieNode
}
func newTrie() *trie {
return &trie{root: &trieNode{}}
}
func (t *trie) Insert(word string) {
cur := t.root
for _, c := range word {
i := c - 'a' // map the letter directly to its slot in the 26-wide array
if cur.children[i] == nil {
cur.children[i] = &trieNode{} // lazily create nodes only for paths that are actually used
}
cur = cur.children[i]
}
cur.isEnd = true // only the final letter's node is flagged as a complete word
}
// find walks as far down the shared prefix path as the string allows, and is
// shared by both Search and StartsWith since they differ only in what they
// check once they reach the end of the path (isEnd vs simply existing)
func (t *trie) find(word string) *trieNode {
cur := t.root
for _, c := range word {
i := c - 'a'
if cur.children[i] == nil {
return nil // path breaks partway through: neither a word nor a prefix exists
}
cur = cur.children[i]
}
return cur
}
func (t *trie) Search(word string) bool {
n := t.find(word)
// reaching the node isn't enough on its own: "gop" reaches a real node in
// this trie (via "gopher") but was never inserted as its own word
return n != nil && n.isEnd
}
func (t *trie) StartsWith(prefix string) bool {
return t.find(prefix) != nil // merely existing on the path is enough, isEnd is irrelevant here
}
func main() {
t := newTrie()
words := []string{"go", "gopher", "golang", "goroutine", "channel"}
for _, w := range words {
t.Insert(w)
}
fmt.Println("Search(go):", t.Search("go"))
fmt.Println("Search(gop):", t.Search("gop"))
fmt.Println("StartsWith(gop):", t.StartsWith("gop"))
fmt.Println("Search(golang):", t.Search("golang"))
fmt.Println("StartsWith(chan):", t.StartsWith("chan"))
fmt.Println("StartsWith(chart):", t.StartsWith("chart"))
fmt.Println("Search(channel):", t.Search("channel"))
}
17. Graph as an adjacency list with BFS traversal
Represent an undirected graph as a map[string][]string adjacency list and perform a breadth-first traversal from a given starting node using a queue.
package main
import "fmt"
type graph struct {
adj map[string][]string
}
func newGraph() *graph {
return &graph{adj: make(map[string][]string)}
}
// undirected graph: every edge is recorded on both endpoints' adjacency lists
func (g *graph) addEdge(u, v string) {
g.adj[u] = append(g.adj[u], v)
g.adj[v] = append(g.adj[v], u)
}
func (g *graph) bfs(start string) []string {
// start is marked visited immediately (not when dequeued) so it can never
// be queued a second time by another node that also points to it
visited := map[string]bool{start: true}
queue := []string{start}
var order []string
for len(queue) > 0 {
// FIFO queue is what makes this breadth-first: all nodes at the current
// distance are visited before any node one hop further out
node := queue[0]
queue = queue[1:]
order = append(order, node)
for _, next := range g.adj[node] {
// mark visited at discovery time, not dequeue time, so the same
// neighbor reached via two different paths is only queued once
if !visited[next] {
visited[next] = true
queue = append(queue, next)
}
}
}
return order
}
func main() {
g := newGraph()
edges := [][2]string{
{"A", "B"}, {"A", "C"}, {"B", "D"},
{"C", "D"}, {"D", "E"}, {"E", "F"},
}
for _, e := range edges {
g.addEdge(e[0], e[1])
}
fmt.Println("BFS from A:", g.bfs("A"))
fmt.Println("BFS from E:", g.bfs("E"))
}
18. Graph as an adjacency list with DFS traversal
Using the same adjacency-list representation, perform an iterative depth-first traversal with an explicit stack instead of recursion.
package main
import "fmt"
type graph struct {
adj map[string][]string
}
func newGraph() *graph {
return &graph{adj: make(map[string][]string)}
}
func (g *graph) addEdge(u, v string) {
g.adj[u] = append(g.adj[u], v)
g.adj[v] = append(g.adj[v], u)
}
// dfs performs an iterative depth-first traversal using an explicit stack:
// swapping bfs's queue for a LIFO stack is the only structural change needed
// to turn breadth-first into depth-first, since it's the pop order that
// decides whether we fan out sideways or plunge down one path first.
func (g *graph) dfs(start string) []string {
visited := map[string]bool{}
stack := []string{start}
var order []string
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// unlike the BFS version, duplicates can still be pushed onto the stack
// before being marked visited, so the check has to happen here on pop,
// not at push time, and repeats are just skipped rather than prevented
if visited[node] {
continue
}
visited[node] = true
order = append(order, node)
neighbors := g.adj[node]
// push neighbors in reverse order so that, since a stack pops last-in-
// first-out, they still get visited in their original left-to-right order
for i := len(neighbors) - 1; i >= 0; i-- {
if !visited[neighbors[i]] {
stack = append(stack, neighbors[i])
}
}
}
return order
}
func main() {
g := newGraph()
edges := [][2]string{
{"A", "B"}, {"A", "C"}, {"B", "D"},
{"C", "D"}, {"D", "E"}, {"E", "F"},
}
for _, e := range edges {
g.addEdge(e[0], e[1])
}
fmt.Println("DFS from A:", g.dfs("A"))
fmt.Println("DFS from E:", g.dfs("E"))
}
19. Check whether a binary tree is height-balanced
Determine whether every node's left and right subtree heights differ by at most 1, computing heights and detecting imbalance in a single bottom-up pass.
package main
import "fmt"
type tnode struct {
val int
left, right *tnode
}
// height returns -1 as a sentinel through balanced to short-circuit
// once an imbalance is found anywhere in the subtree.
func height(n *tnode, balanced *bool) int {
if n == nil {
return 0
}
lh := height(n.left, balanced)
rh := height(n.right, balanced)
diff := lh - rh
// AVL-style balance invariant: sibling subtree heights may differ
// by at most 1, at every node, not just at the root.
if diff < -1 || diff > 1 {
*balanced = false
}
if lh > rh {
return lh + 1
}
return rh + 1
}
func isBalanced(root *tnode) bool {
balanced := true
height(root, &balanced)
return balanced
}
func main() {
// Balanced tree:
// 4
// / \
// 2 6
// / \ / \
// 1 3 5 7
balancedTree := &tnode{4,
&tnode{2, &tnode{val: 1}, &tnode{val: 3}},
&tnode{6, &tnode{val: 5}, &tnode{val: 7}},
}
// Skewed / unbalanced tree: 1 -> 2 -> 3 -> 4 (all left children)
unbalancedTree := &tnode{val: 1, left: &tnode{val: 2, left: &tnode{val: 3, left: &tnode{val: 4}}}}
fmt.Println("balancedTree isBalanced:", isBalanced(balancedTree))
fmt.Println("unbalancedTree isBalanced:", isBalanced(unbalancedTree))
fmt.Println("nil tree isBalanced:", isBalanced(nil))
}
20. Level-order (BFS) traversal of a binary tree
Traverse a binary tree breadth-first and return its values grouped as a slice of slices, one inner slice per depth level.
package main
import "fmt"
type tnode struct {
val int
left, right *tnode
}
// levelOrder returns node values grouped level by level using a
// queue and a per-level size snapshot.
func levelOrder(root *tnode) [][]int {
if root == nil {
return nil
}
var levels [][]int
queue := []*tnode{root}
for len(queue) > 0 {
// Snapshot the queue length *before* enqueuing this level's
// children: children get pushed onto the same queue, so without
// freezing size up front the inner loop would drift into the
// next level instead of stopping at the current one.
size := len(queue)
level := make([]int, 0, size)
for i := 0; i < size; i++ {
// Dequeue from the front (FIFO) so nodes are visited in the
// order they were discovered, which is what gives BFS its
// level-by-level shape.
n := queue[0]
queue = queue[1:]
level = append(level, n.val)
if n.left != nil {
queue = append(queue, n.left)
}
if n.right != nil {
queue = append(queue, n.right)
}
}
levels = append(levels, level)
}
return levels
}
func main() {
root := &tnode{1,
&tnode{2, &tnode{val: 4}, &tnode{val: 5}},
&tnode{3, nil, &tnode{val: 6}},
}
for i, level := range levelOrder(root) {
fmt.Printf("level %d: %v\n", i, level)
}
}
21. Lowest common ancestor of two nodes in a binary search tree
Given a BST and two values, find their lowest common ancestor by walking down from the root and exploiting BST ordering, without building parent pointers.
package main
import "fmt"
type tnode struct {
val int
left, right *tnode
}
func insert(root *tnode, v int) *tnode {
if root == nil {
return &tnode{val: v}
}
if v < root.val {
root.left = insert(root.left, v)
} else {
// Duplicates fall through to the right subtree; this input
// has no repeats, so it doesn't matter here, but the ordering
// invariant the LCA search below relies on still holds either way.
root.right = insert(root.right, v)
}
return root
}
// lowestCommonAncestor exploits the BST ordering property: the LCA is the
// first node where p and q's values fall on opposite sides (or match it).
func lowestCommonAncestor(root *tnode, p, q int) *tnode {
cur := root
for cur != nil {
switch {
case p < cur.val && q < cur.val:
cur = cur.left
case p > cur.val && q > cur.val:
cur = cur.right
default:
// p and q no longer agree on a side (one is <= cur.val and
// the other is >=), or cur itself equals one of them: this
// is the deepest node both still descend from, so it's the LCA.
return cur
}
}
return nil
}
func main() {
var root *tnode
for _, v := range []int{8, 3, 10, 1, 6, 14, 4, 7, 13} {
root = insert(root, v)
}
lca1 := lowestCommonAncestor(root, 4, 7)
fmt.Println("LCA(4, 7):", lca1.val)
lca2 := lowestCommonAncestor(root, 1, 7)
fmt.Println("LCA(1, 7):", lca2.val)
lca3 := lowestCommonAncestor(root, 3, 14)
fmt.Println("LCA(3, 14):", lca3.val)
lca4 := lowestCommonAncestor(root, 13, 14)
fmt.Println("LCA(13, 14):", lca4.val)
}
22. Implement a circular (ring) buffer with fixed capacity
Build a fixed-capacity ring buffer that, once full, overwrites the oldest element on Push rather than rejecting the write; supports Push, Pop, and a Snapshot of current contents in order.
package main
import "fmt"
// ringBuffer is a fixed-capacity circular buffer. When full, Push
// overwrites the oldest element rather than rejecting the write.
type ringBuffer struct {
data []int
head int // index of oldest element
size int
cap int
}
func newRingBuffer(capacity int) *ringBuffer {
return &ringBuffer{data: make([]int, capacity), cap: capacity}
}
func (r *ringBuffer) Push(v int) {
// tail is derived from head+size (mod cap) instead of stored
// separately, and size is tracked explicitly rather than inferred
// from head==tail, because head==tail alone can't tell an empty
// buffer apart from a full one.
tail := (r.head + r.size) % r.cap
r.data[tail] = v
if r.size < r.cap {
r.size++
} else {
// full: overwrite oldest, advance head
r.head = (r.head + 1) % r.cap
}
}
func (r *ringBuffer) Pop() (int, bool) {
if r.size == 0 {
return 0, false
}
v := r.data[r.head]
r.head = (r.head + 1) % r.cap
r.size--
return v, true
}
func (r *ringBuffer) Snapshot() []int {
out := make([]int, 0, r.size)
for i := 0; i < r.size; i++ {
out = append(out, r.data[(r.head+i)%r.cap])
}
return out
}
func main() {
rb := newRingBuffer(3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
fmt.Println("after pushing 1,2,3:", rb.Snapshot())
rb.Push(4) // overwrites 1 (oldest)
fmt.Println("after pushing 4 (full, overwrite oldest):", rb.Snapshot())
v, ok := rb.Pop()
fmt.Println("pop:", v, ok)
fmt.Println("after pop:", rb.Snapshot())
rb.Push(5)
rb.Push(6)
fmt.Println("after pushing 5,6:", rb.Snapshot())
}
23. Implement a union-find / disjoint-set data structure
Build a disjoint-set structure with path compression on Find and union by rank on Union, and track the number of distinct sets as merges happen.
package main
import "fmt"
// unionFind implements disjoint-set with path compression and union by rank.
type unionFind struct {
parent []int
rank []int
count int // number of distinct sets
}
func newUnionFind(n int) *unionFind {
uf := &unionFind{parent: make([]int, n), rank: make([]int, n), count: n}
for i := range uf.parent {
uf.parent[i] = i
}
return uf
}
func (uf *unionFind) Find(x int) int {
if uf.parent[x] != x {
// path compression: rewire x straight to the set's root as we
// unwind, so every future Find along this path is O(1) instead
// of re-walking the same chain
uf.parent[x] = uf.Find(uf.parent[x])
}
return uf.parent[x]
}
func (uf *unionFind) Union(a, b int) bool {
ra, rb := uf.Find(a), uf.Find(b)
if ra == rb {
return false
}
// Union by rank: always hang the shorter (or equal) tree under the
// taller one's root, so ra ends up the root either way. This keeps
// tree depth logarithmic instead of letting chains of unions build
// a long, slow-to-Find spine. Rank only grows when two trees of
// equal height merge, since that's the one case that increases the
// worst-case depth of the result.
switch {
case uf.rank[ra] < uf.rank[rb]:
ra, rb = rb, ra
case uf.rank[ra] == uf.rank[rb]:
uf.rank[ra]++
}
uf.parent[rb] = ra
uf.count--
return true
}
func (uf *unionFind) Connected(a, b int) bool {
return uf.Find(a) == uf.Find(b)
}
func main() {
uf := newUnionFind(10)
edges := [][2]int{{0, 1}, {1, 2}, {3, 4}, {5, 6}, {6, 7}, {7, 8}}
for _, e := range edges {
uf.Union(e[0], e[1])
}
fmt.Println("Connected(0, 2):", uf.Connected(0, 2))
fmt.Println("Connected(0, 3):", uf.Connected(0, 3))
fmt.Println("Connected(5, 8):", uf.Connected(5, 8))
fmt.Println("Connected(4, 9):", uf.Connected(4, 9))
fmt.Println("distinct sets before merging 2-3:", uf.count)
uf.Union(2, 3)
fmt.Println("Connected(0, 4) after union(2,3):", uf.Connected(0, 4))
fmt.Println("distinct sets after merging 2-3:", uf.count)
}
24. Validate whether a binary tree is a valid binary search tree
Check the BST invariant correctly by threading a valid (min, max) range down through the recursion, rather than only comparing a node against its immediate parent.
package main
import (
"fmt"
"math"
)
type tnode struct {
val int
left, right *tnode
}
// isValidBST checks the BST invariant by threading a valid (min, max)
// range down through the recursion instead of just comparing children.
func isValidBST(n *tnode, min, max int) bool {
if n == nil {
return true
}
if n.val <= min || n.val >= max {
return false
}
// Left child inherits the same floor but its ceiling tightens to
// n.val; the right child inherits the same ceiling but its floor
// rises to n.val. That's how every deeper node stays constrained
// by every ancestor above it, not just its direct parent.
return isValidBST(n.left, min, n.val) && isValidBST(n.right, n.val, max)
}
func validate(root *tnode) bool {
// Root has no ancestors yet, so seed the range with the widest
// possible bounds instead of a sentinel like 0 that could collide
// with a real node value.
return isValidBST(root, math.MinInt, math.MaxInt)
}
func main() {
// Valid BST:
// 5
// / \
// 3 8
// / \ \
// 1 4 9
valid := &tnode{5,
&tnode{3, &tnode{val: 1}, &tnode{val: 4}},
&tnode{8, nil, &tnode{val: 9}},
}
// Invalid: right subtree's left child (3) satisfies "< its parent 8"
// but violates the ancestor rule (must be > root 5), so a naive
// "compare with immediate parent only" check would miss it.
invalid := &tnode{5,
&tnode{3, &tnode{val: 1}, &tnode{val: 4}},
&tnode{8, &tnode{val: 3}, &tnode{val: 9}},
}
// Invalid: duplicate value equal to an ancestor.
dup := &tnode{5,
&tnode{val: 5},
&tnode{val: 8},
}
fmt.Println("valid tree isValidBST:", validate(valid))
fmt.Println("invalid tree isValidBST:", validate(invalid))
fmt.Println("duplicate-value tree isValidBST:", validate(dup))
}
1. Print odd and even numbers alternately using one channel
Two goroutines take turns printing 1..N, handing off control with a single unbuffered channel used as a strict ping-pong baton.
package main
import (
"fmt"
"sync"
)
func main() {
const n = 10
// unbuffered: a send only completes once the other goroutine is ready to
// receive, so this channel works as a synchronous "baton" rather than a
// data pipe -- struct{}{} carries no payload because only the handoff
// itself matters
ch := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2)
go func() { // prints odd numbers; holds the baton first since i starts at 1
defer wg.Done()
for i := 1; i <= n; i += 2 {
fmt.Println(i)
// hand the baton to the even goroutine -- this blocks until it's
// received, which is what pins "odd printed" before "even printed"
ch <- struct{}{}
if i+2 <= n {
// only wait for the baton back if another odd number remains --
// on the last iteration the even goroutine has already sent its
// final number and exits without sending again, so receiving
// unconditionally here would deadlock
<-ch
}
}
}()
go func() { // prints even numbers; always waits for the baton before printing
defer wg.Done()
for i := 2; i <= n; i += 2 {
<-ch
fmt.Println(i)
if i+2 <= n {
// same guard as the odd side: don't send the baton back once
// there's no odd number left to receive it
ch <- struct{}{}
}
}
}()
wg.Wait()
}
2. Print odd and even numbers alternately using two channels
The same alternating-print problem, but each goroutine gets its own "your turn" channel instead of sharing one — one channel signals the odd goroutine, the other signals the even goroutine.
package main
import "fmt"
func main() {
const n = 10
// two dedicated, unbuffered channels instead of one shared channel --
// each direction of the handoff gets its own name, so which goroutine's
// turn it is can never be ambiguous the way it would be if both sides
// sent and received on the same channel
oddTurn := make(chan struct{})
evenTurn := make(chan struct{})
// a separate completion signal is needed because main isn't one of the
// two ping-ponging goroutines -- it has no turn channel of its own to
// block on once the last number has been printed
done := make(chan struct{})
go func() { // waits for oddTurn, prints, signals evenTurn
for i := 1; i <= n; i += 2 {
<-oddTurn
fmt.Println(i)
evenTurn <- struct{}{}
}
}()
go func() { // waits for evenTurn, prints, signals oddTurn (or done)
for i := 2; i <= n; i += 2 {
<-evenTurn
fmt.Println(i)
if i < n {
oddTurn <- struct{}{}
} else {
// last number printed -- signal done instead of oddTurn, since
// sending on oddTurn here would have no receiver left (the odd
// goroutine's loop has already ended) and would block forever
done <- struct{}{}
}
}
}()
// neither goroutine can proceed until someone sends the first token, so
// main primes the sequence itself before waiting on the finish line
oddTurn <- struct{}{} // kick off the first turn
<-done
}
3. Producer-consumer with a buffered channel
One producer goroutine fills a buffered channel; a consumer drains it in FIFO order. Only the consumer prints, so channel ordering — not goroutine scheduling — fully determines the output.
package main
import "fmt"
func main() {
const items = 6
// buffered: producer can run ahead of consumer, filling up to 3 slots
// before it has to block on a send -- with items=6 > capacity=3 the
// producer still catches up to the consumer partway through, so the
// buffer smooths bursts but doesn't remove backpressure entirely
buf := make(chan int, 3)
go func() { // producer: only sends, never prints (avoids racing consumer's prints)
for i := 1; i <= items; i++ {
buf <- i
}
// closing tells the consumer's range loop there's nothing more coming --
// without this close, range over buf would block forever after the
// last value instead of exiting
close(buf)
}()
for v := range buf { // consumer: sole printer, drains in FIFO order
// a channel delivers values in the order they were sent and there's
// exactly one goroutine printing them, so "consumed: 1..6" in order is
// guaranteed -- there's no second reader that could steal an item out
// of sequence
fmt.Println("consumed:", v)
}
fmt.Println("done")
}
4. Worker pool: process jobs concurrently, collect results in order
Three worker goroutines pull jobs from a shared channel and compute squares concurrently. Workers never print — they write results into a job-indexed slot, which the main goroutine prints back in strict job order.
package main
import (
"fmt"
"sync"
)
// jobs is receive-only and results is send-only from the worker's point of
// view -- the compiler then rejects any attempt to send on jobs or read from
// results here, so a worker can only ever consume work and produce output
func worker(id int, jobs <-chan int, results chan<- [2]int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs { // exits once jobs is closed and drained, letting wg.Done fire
results <- [2]int{j, j * j} // [job, result] pair, no printing here
}
}
func main() {
const numJobs = 9
const numWorkers = 3
// both channels sized to hold every job/result up front, so the sending
// side (main filling jobs, workers filling results) never has to block
// waiting on the other side to drain -- that keeps the handoff logic
// below simple regardless of how fast workers happen to run
jobs := make(chan int, numJobs)
results := make(chan [2]int, numJobs)
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
// all workers read from the same jobs channel, so which worker handles
// which job is nondeterministic (first one free grabs the next job) --
// that's fine since nothing here depends on processing order, only on
// the final printed order (restored below)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // no more jobs -- lets each worker's range loop end once drained
go func() {
// closing results must wait until every worker is done sending, but
// that can't happen on main's goroutine: main still needs to range
// over results below, and blocking main here on wg.Wait() first would
// make the two steps run in the wrong order relative to draining --
// running it in its own goroutine lets results be closed the moment
// workers finish, concurrently with main already consuming
wg.Wait()
close(results)
}()
out := make([]int, numJobs+1) // index by job number
for r := range results { // drains in whatever order workers finished, not job order
out[r[0]] = r[1] // stash by job number so completion order can't leak into the output
}
for j := 1; j <= numJobs; j++ { // single printer, strict order
// iterating 1..numJobs here (instead of printing as results arrive)
// is what makes the output deterministic despite the nondeterministic
// worker scheduling above
fmt.Printf("job %d -> %d\n", j, out[j])
}
}
5. Fan-in: merge multiple channels into one
Three source goroutines each emit three numbers into their own channel; a fan-in stage merges all of them into a single channel. Since arrival order across channels is inherently non-deterministic, the merged values are sorted before printing.
package main
import (
"fmt"
"sort"
"sync"
)
// returns a receive-only channel so callers can't accidentally send on (or
// close) a channel they don't own the sending side of
func source(base int) <-chan int {
out := make(chan int)
go func() {
// closing once this goroutine is done producing is what lets a
// downstream "for range" over this channel terminate instead of
// blocking forever after the last value
defer close(out)
for i := 1; i <= 3; i++ {
out <- base + i
}
}()
return out
}
func fanIn(chans ...<-chan int) <-chan int {
merged := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chans)) // set the full count before any forwarder starts, so Wait can't return early
for _, c := range chans {
// one independent forwarder goroutine per input channel -- they all
// send to the same merged channel with no coordination between them,
// which is exactly why the interleaving of a/b/c values is a race and
// not reproducible run to run
go func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}(c)
}
go func() {
// runs in its own goroutine because it has to block on wg.Wait() until
// every forwarder finishes -- doing that inline in fanIn would block
// the function itself and it could never return merged to the caller
// for draining, which the forwarders need in order to unblock their
// sends and finish in the first place
wg.Wait()
close(merged) // safe: guaranteed no forwarder is still sending once wg.Wait() returns
}()
return merged
}
func main() {
a := source(0)
b := source(10)
c := source(20)
var all []int
for v := range fanIn(a, b, c) { // arrival order is non-deterministic
all = append(all, v)
}
sort.Ints(all) // sort before printing so output is stable every run
fmt.Println("merged (sorted):", all)
}
6. sync.WaitGroup: wait for goroutines to finish
Five goroutines each mark their own slot in a slice as complete; the main goroutine blocks on a WaitGroup until all five call Done, then reports the count.
package main
import (
"fmt"
"sync"
)
func main() {
const numWorkers = 5
var wg sync.WaitGroup
done := make([]bool, numWorkers)
var mu sync.Mutex
for i := 0; i < numWorkers; i++ {
// Add(1) happens before the goroutine is started (not inside it), so
// the counter is guaranteed to already reflect every worker by the
// time wg.Wait() below runs, no matter how the scheduler interleaves
// startup
wg.Add(1)
// i is passed in as id rather than captured from the enclosing scope --
// each goroutine then has its own private copy of the loop value, so it
// can't observe i having already moved on to a later iteration
go func(id int) {
defer wg.Done()
// mu guards the shared done slice; each goroutine only ever writes
// its own index so this particular write can't collide with another
// goroutine's, but locking is the habit that keeps this code correct
// by default rather than by coincidence of which index each worker
// happens to touch
mu.Lock()
done[id] = true
mu.Unlock()
}(i)
}
wg.Wait() // blocks until all workers call Done(); also the point after which their writes to done are guaranteed visible here
count := 0
for _, d := range done {
if d {
count++
}
}
fmt.Printf("%d/%d workers finished\n", count, numWorkers)
fmt.Println("done")
}
7. sync.Once: run an initializer exactly once
Five goroutines all call the same initializer through a shared sync.Once, guaranteeing it runs a single time no matter how many goroutines race to call it.
package main
import (
"fmt"
"sync"
)
func main() {
const numGoroutines = 5
var once sync.Once // guarantees initialize runs exactly once, even with 5 racing callers - no mutex/bool bookkeeping needed
var wg sync.WaitGroup
ran := make([]bool, numGoroutines) // per-goroutine slot, no shared write races
initialize := func() {
fmt.Println("initializing shared resource...")
}
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(initialize) // only the first caller actually runs initialize; the other 4 block here until it finishes, then return without re-running it
ran[id] = true // safe to run after once.Do returns - initialization is guaranteed complete for every goroutine, not just the winner
}(i)
}
wg.Wait() // without this, main could read ran before the goroutines finish writing to it
for i, r := range ran { // single printer, strict order - main is the only reader here, so ran needs no locking of its own
fmt.Printf("goroutine %d saw initialization complete: %v\n", i, r)
}
}
8. Mutex vs atomic: safe concurrent counters
50 goroutines each increment a shared counter 1000 times, once using a sync.Mutex to guard the counter and once using sync/atomic — both approaches are race-free and land on the same correct final value.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
const goroutines = 50
const incrementsEach = 1000
// Mutex-protected counter: mutexCounter++ is a read-modify-write, so without a lock two goroutines
// could read the same value and both write back the same increment, losing an update
var mu sync.Mutex
mutexCounter := 0
var wg1 sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg1.Add(1)
go func() {
defer wg1.Done()
for j := 0; j < incrementsEach; j++ {
mu.Lock() // serializes access so the increment below is never split across two goroutines
mutexCounter++
mu.Unlock()
}
}()
}
wg1.Wait() // must finish before printing, otherwise mutexCounter could still be mid-update
// atomic counter: same workload, but using a lock-free CPU instruction instead of a mutex -
// cheaper than locking for a single int64 since there's no goroutine to block/wake
var atomicCounter int64
var wg2 sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg2.Add(1)
go func() {
defer wg2.Done()
for j := 0; j < incrementsEach; j++ {
atomic.AddInt64(&atomicCounter, 1) // hardware-level fetch-and-add, so the increment can't be interleaved with another goroutine's
}
}()
}
wg2.Wait()
// both print 50000 (50 goroutines * 1000 increments) - proof neither approach dropped an update
fmt.Println("mutex counter:", mutexCounter)
fmt.Println("atomic counter:", atomicCounter)
}
9. Pipeline pattern: generate, square, sum
Three stages — a generator, a squaring stage, and a summing stage — are chained together with channels, each stage running in its own goroutine, producing a single deterministic result.
package main
import "fmt"
// generate returns immediately with an unbuffered channel; the values are produced by a goroutine
// running concurrently with whatever reads from the channel, so production and consumption overlap
func generate(n int) <-chan int {
out := make(chan int) // unbuffered: each send blocks until square's goroutine is ready to receive it
go func() {
defer close(out) // closing signals "no more values" so downstream range loops terminate instead of blocking forever
for i := 1; i <= n; i++ {
out <- i
}
}()
return out // return the receive-only end before the goroutine finishes - caller consumes as values arrive
}
// square is the middle pipeline stage: it fans values through unchanged in shape (int -> int),
// only transforming each one, so stages can chain without knowing about their neighbors' internals
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in { // range over a channel exits automatically once in is closed and drained
out <- v * v
}
}()
return out
}
// sum is the terminal stage: it collapses the stream into a single value, sent only once the
// upstream channel is closed and every value has been accumulated
func sum(in <-chan int) <-chan int {
out := make(chan int) // still a channel (not a plain return value) so main can block-receive the final result
go func() {
defer close(out)
total := 0
for v := range in {
total += v
}
out <- total // only reached after the range loop above sees in closed, i.e. after square/generate are done
}()
return out
}
func main() {
const n = 5
// pipeline: generate 1..n -> square each -> sum all; three goroutines run concurrently, linked by
// unbuffered channels, so values flow through the moment each stage is ready rather than in batches
result := <-sum(square(generate(n))) // blocks until the sum goroutine sends its single final value
fmt.Printf("sum of squares 1..%d = %d\n", n, result)
}
10. context cancellation: stop a goroutine on demand
A worker goroutine blocks on ctx.Done() and only proceeds once the context is cancelled, then reports the exact reason it stopped via ctx.Err().
package main
import (
"context"
"fmt"
)
// worker only accepts the receive-only Done() channel via ctx - it has no way to cancel itself,
// keeping the "who can cancel" decision entirely with whoever holds the cancel function
func worker(ctx context.Context, stopped chan<- string) {
<-ctx.Done() // Done() is closed (not sent to) on cancellation, so every waiting goroutine wakes up, not just one
stopped <- fmt.Sprintf("worker stopped: %v", ctx.Err()) // ctx.Err() reports why it was cancelled, e.g. "context canceled" vs a deadline timeout
}
func main() {
ctx, cancel := context.WithCancel(context.Background()) // cancel is the only handle that can close ctx.Done() - the worker never sees it
stopped := make(chan string) // unbuffered handshake channel: lets main block until the worker has actually observed the cancellation
go worker(ctx, stopped)
fmt.Println("worker started")
cancel() // cancel the context, signalling the worker to stop; safe to call more than once and from any goroutine
fmt.Println(<-stopped) // blocks here until worker's <-ctx.Done() unblocks and it sends its message - proves the signal was received, not just sent
}
11. Non-blocking channel check with select/default
A select statement with a default case checks whether a channel has a value ready without blocking, falling through immediately when nothing is available.
package main
import "fmt"
// a select with only one case would block until ch has a value; adding default makes the receive
// non-blocking - if no value is ready right now, default runs instead of waiting
func tryRecv(ch <-chan int, label string) {
select {
case v := <-ch: // only taken if a value is immediately available to receive
fmt.Printf("%s: got value %d\n", label, v)
default: // runs immediately when ch is empty, so the caller never stalls waiting for a sender
fmt.Printf("%s: channel empty, moving on\n", label)
}
}
func main() {
ch := make(chan int, 1) // buffered so a send can complete without a receiver already waiting
tryRecv(ch, "check 1") // channel is empty, so this hits default rather than blocking
ch <- 42 // buffer has room, so this send returns immediately instead of waiting for a receiver
tryRecv(ch, "check 2") // channel now has a value; the receive case fires and also drains it
tryRecv(ch, "check 3") // value was drained by check 2, empty again - proves select/default doesn't accidentally leave the value behind
}
12. Print numbers 1..N in order using N goroutines
Eight goroutines each own exactly one number; a chain of per-step "baton" channels forces each goroutine to wait for the previous one before printing, guaranteeing strict numeric order.
package main
import (
"fmt"
"sync"
)
func main() {
const n = 8
// n+1 unbuffered channels form a chain: batons[i-1] wakes goroutine i, which then wakes i+1 via
// batons[i] - each channel is a one-shot baton passed exactly once, never reused
batons := make([]chan struct{}, n+1)
for i := range batons {
batons[i] = make(chan struct{}) // struct{}{} carries no data - the channel is used purely as a signal, not to transfer a value
}
var wg sync.WaitGroup
wg.Add(n) // registered up front so main's wg.Wait() below always has the right count to wait for
for i := 1; i <= n; i++ {
go func(id int) {
defer wg.Done()
<-batons[id-1] // blocks here - all n goroutines start immediately but only one is unblocked at a time, so they can't race each other to print
fmt.Println(id) // this goroutine's exclusive turn to print
if id < n {
batons[id] <- struct{}{} // pass the baton to the next goroutine; the last goroutine (id == n) has no successor to unblock
}
}(i)
}
batons[0] <- struct{}{} // kick off goroutine 1; without this nothing would ever unblock and the program would deadlock
wg.Wait() // blocks until all n goroutines have printed and returned, guaranteeing the full 1..n sequence lands before main exits
}
13. Bound Concurrent Work with a Semaphore
Limit how many goroutines are allowed to do work at the same time using a buffered channel as a counting semaphore, so at most a fixed number of jobs ever run concurrently.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
const numJobs = 6
const maxConcurrent = 2
// a buffered channel used purely for its capacity: the buffer size caps how many
// goroutines can hold a "token" at once, so it works as a counting semaphore
sem := make(chan struct{}, maxConcurrent)
// pre-sized and written by index (not appended) so each goroutine can write its
// own slot without racing with the others, no locking needed for this part
results := make([]string, numJobs)
var wg sync.WaitGroup
for i := 1; i <= numJobs; i++ {
wg.Add(1)
// i is passed in as job (not captured directly) so each goroutine gets
// its own private copy of the loop value to write into results with
go func(job int) {
defer wg.Done()
sem <- struct{}{} // acquire a slot in the semaphore
// deferred (not a plain call at the end) so the slot is freed even if the
// goroutine panics after this point, otherwise a panic would leak a token
// and permanently shrink the effective concurrency limit
defer func() { <-sem }() // release the slot when done
time.Sleep(20 * time.Millisecond) // simulate work
// writing to results[job-1] instead of appending means the final printed
// order is job 1..N regardless of which goroutine actually finishes first
results[job-1] = fmt.Sprintf("job %d: done (ran with at most %d jobs active)", job, maxConcurrent)
}(i)
}
// blocks until every goroutine has called wg.Done(); this also establishes a
// happens-before edge so all the writes into results above are visible here
wg.Wait()
fmt.Printf("semaphore capacity: %d concurrent job(s) allowed\n", maxConcurrent)
for _, r := range results {
fmt.Println(r)
}
fmt.Println("all jobs completed")
}
14. Timeout a Result with select and time.After
Wait for an asynchronous result but give up after a fixed deadline, using select to race the result channel against a time.After channel.
package main
import (
"fmt"
"time"
)
// fetch simulates work that takes `delay` before it has a result ready.
func fetch(delay time.Duration) <-chan string {
// buffered with capacity 1 so the send below never blocks: if the caller
// times out and stops listening, this goroutine can still deliver its
// value into the buffer and exit instead of leaking forever
out := make(chan string, 1)
go func() {
time.Sleep(delay)
out <- "result ready"
}()
return out
}
// fetchWithTimeout waits for the fetch to finish, but gives up after `timeout`.
func fetchWithTimeout(delay, timeout time.Duration) string {
// select blocks on both channels at once and proceeds with whichever fires
// first, so the "fetch finished" and "timeout" cases race fairly instead
// of one being checked before the other
select {
case res := <-fetch(delay):
return res
case <-time.After(timeout):
// time.After starts a fresh timer each call and returns its channel;
// no cleanup is needed here since fetchWithTimeout returns right after --
// as of Go 1.23+ an unstopped, undrained timer is still garbage collected
// once unreachable, so this doesn't leak the way it would have in older Go
return "timed out waiting for result"
}
}
func main() {
const timeout = 50 * time.Millisecond
fmt.Println("call A: work finishes in 10ms, timeout is 50ms")
fmt.Println(" ->", fetchWithTimeout(10*time.Millisecond, timeout))
fmt.Println("call B: work finishes in 200ms, timeout is 50ms")
fmt.Println(" ->", fetchWithTimeout(200*time.Millisecond, timeout))
}
15. Broadcast a Stop Signal by Closing a Done Channel
Tell many goroutines to stop at once by closing a single shared "done" channel — every goroutine's receive on that channel unblocks simultaneously, unlike sending a value which only one receiver would get.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
const numWorkers = 4
// closing this broadcasts "stop" to every worker: a send on done could only
// ever wake one waiting receiver, but a closed channel makes every pending
// and future receive return immediately, so all N workers wake up together
done := make(chan struct{})
// indexed by id-1 (not appended) so printing in slice order below is the
// same as worker id order, independent of the order workers actually exit
results := make([]string, numWorkers)
var wg sync.WaitGroup
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
// i passed in as id gives each goroutine its own copy to index results with
go func(id int) {
defer wg.Done()
for {
// the default case makes this select non-blocking: on each loop the
// worker peeks at done without waiting, so it can keep "working" in
// between checks instead of parking on the receive
select {
case <-done:
results[id-1] = fmt.Sprintf("worker %d: stop signal received, exiting", id)
return
default:
time.Sleep(5 * time.Millisecond) // stand-in for a unit of work; also throttles the loop so it polls done instead of spinning the CPU
}
}
}(i)
}
time.Sleep(30 * time.Millisecond) // let workers tick for a bit
fmt.Println("main: broadcasting stop to all workers")
close(done) // every worker's <-done case unblocks at once
wg.Wait()
for _, r := range results {
fmt.Println(r)
}
fmt.Println("main: all workers stopped")
}
16. Rate Limit Actions with time.Ticker
Throttle how often an action can happen by only allowing it through once per tick of a time.Ticker, so no matter how fast requests arrive, they're processed no faster than the tick rate.
package main
import (
"fmt"
"time"
)
func main() {
const numRequests = 5
// the ticker's channel receives a value every 10ms; that fixed cadence is
// what enforces the rate limit below, not any locking or counting
limiter := time.NewTicker(10 * time.Millisecond)
// Stop releases the ticker's internal timer resource; without it the ticker
// would keep firing in the background for the life of the program
defer limiter.Stop()
// sized to hold all requests up front and then closed, so it's just a
// pre-filled queue for range to drain in order — there's no producer
// goroutine here, so this isn't guarding against blocking sends
requests := make(chan int, numRequests)
for i := 1; i <= numRequests; i++ {
requests <- i
}
close(requests)
// Every request must wait for a tick before it is allowed through,
// so actions never happen more often than one per tick.
for req := range requests {
// blocking receive: this is what actually paces the loop to one
// iteration per tick instead of running as fast as possible
<-limiter.C
fmt.Printf("request %d: allowed through at tick\n", req)
}
fmt.Println("rate limiter: all requests processed, one per tick")
}
17. sync.RWMutex: Many Readers, One Writer
Guard shared state so multiple goroutines can read it concurrently, but a write always has exclusive access — implemented with sync.RWMutex's RLock/RUnlock and Lock/Unlock.
package main
import (
"fmt"
"sync"
)
type SafeConfig struct {
mu sync.RWMutex
value int
}
func (c *SafeConfig) Read() int {
// RLock lets any number of readers in at once (unlike a plain Mutex, which
// would serialize them); it only blocks if a writer currently holds Lock
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
func (c *SafeConfig) Write(v int) {
// the exclusive Lock (not RLock) is what makes this safe to mix with
// Read: it waits for every current reader to RUnlock first, and blocks
// any new RLock until this Unlock, so a write can never be torn or seen
// half-applied by a concurrent read
c.mu.Lock()
defer c.mu.Unlock()
c.value = v
}
func main() {
cfg := &SafeConfig{}
const numReaders = 4
// Phase 1: a single writer sets the initial value.
cfg.Write(100)
fmt.Println("writer: set value to 100")
// Phase 2: many readers may hold the RLock at the same time.
// They all observe the same value since no writer runs concurrently with them.
// reads is indexed by id-1 (not appended) purely so the printed order below
// matches reader id order, regardless of which goroutine actually ran first
reads := make([]string, numReaders)
var wg sync.WaitGroup
for i := 1; i <= numReaders; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
v := cfg.Read()
reads[id-1] = fmt.Sprintf("reader %d: read value %d", id, v)
}(i)
}
wg.Wait()
for _, r := range reads {
fmt.Println(r)
}
// Phase 3: the writer updates the value again; RWMutex ensures this
// waits until all in-flight readers from phase 2 are done.
// wg.Wait() above already guarantees phase 2's readers finished, so this
// Lock call is uncontended here — the comment holds for the general case
// where a writer arrives while readers are still active
cfg.Write(200)
fmt.Println("writer: set value to 200")
fmt.Println("final read:", cfg.Read())
}
18. sync.Cond: Wait for a Condition to Become True
Have one goroutine block until another goroutine changes shared state and signals it, using sync.Cond's Wait/Signal instead of a busy-loop or a channel.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
// a Cond is always paired with a Locker: Wait() needs the lock to check the
// predicate (ready) safely, and Signal/Broadcast need it to change that
// same state safely from the other goroutine
cond := sync.NewCond(&mu)
ready := false
var wg sync.WaitGroup
wg.Add(1)
// closing this is how main confirms the waiter has actually started and
// locked mu before main goes on to flip ready and Signal - without it main
// could race ahead and signal before anyone is listening
waitingPrinted := make(chan struct{})
go func() {
defer wg.Done()
mu.Lock()
fmt.Println("waiter: waiting for condition to become true")
close(waitingPrinted)
// cond.Wait() atomically unlocks mu and suspends the goroutine, then
// re-locks mu before returning - that's what makes it safe to re-check
// ready here without a lost-wakeup gap between unlocking and sleeping
for !ready { // predicate loop guards against spurious/early wakeups
cond.Wait()
}
fmt.Println("waiter: condition is true, proceeding")
mu.Unlock()
}()
<-waitingPrinted // make sure the waiter has printed and is entering Wait
// Wait() only guarantees the waiter reached Println/close above, not that it
// has gotten past cond.Wait()'s internal unlock; this sleep buys it that
// time. Unlike a channel send, a Signal delivered before Wait() begins
// listening has no buffer to sit in and is simply lost forever
time.Sleep(20 * time.Millisecond) // give it time to actually reach cond.Wait()
// the state change must happen under the same lock the waiter checks
// ready with, otherwise the waiter could observe a torn/partial update
mu.Lock()
ready = true
fmt.Println("signaler: state changed, signaling waiter")
mu.Unlock()
// Signal wakes at most one goroutine parked in Wait (Broadcast would wake
// all of them); called after Unlock here purely as a minor optimization so
// the woken waiter isn't immediately blocked again re-acquiring mu
cond.Signal()
wg.Wait()
fmt.Println("main: done")
}
19. Worker Pool that Stops Early via Context Cancellation
Run a pool of workers against a batch of jobs, but cancel a shared context mid-flight so workers stop picking up new jobs immediately, leaving the remaining jobs unprocessed instead of running to completion.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
const numWorkers = 3
const jobsPerWorker = 3
// Each job has its own slot, and only the worker that owns that job ever
// writes to it, so this needs no mutex despite being shared across goroutines.
// Printing walks the slice by index afterward, so output order is fixed
// by job number, not by whichever goroutine happens to finish first.
status := make([]string, numWorkers*jobsPerWorker)
// WithCancel gives every worker a shared ctx.Done() channel: calling cancel()
// once closes it, and a closed channel is readable by any number of goroutines
// at once, which is what makes it a cheap one-to-many broadcast.
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func(worker int) {
defer wg.Done()
for k := 0; k < jobsPerWorker; k++ {
job := worker*jobsPerWorker + k + 1
// The default case makes this a non-blocking poll: if ctx.Done() isn't
// closed yet, fall straight through to the work instead of waiting on it.
select {
case <-ctx.Done():
status[job-1] = fmt.Sprintf("job %d: skipped (context cancelled)", job)
continue
default:
}
time.Sleep(100 * time.Millisecond) // simulate work
status[job-1] = fmt.Sprintf("job %d: completed", job)
}
}(w)
}
// Cancel while every worker is still busy with its first job (100ms).
// Each worker only checks ctx.Done() *between* jobs, so job 1 always
// finishes, but jobs 2 and 3 are skipped once cancellation lands.
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
// Wait for every worker to finish writing its slots (completed or skipped)
// before reading the slice back; without this the read here would race
// with the still-running writes above.
wg.Wait()
for _, s := range status {
fmt.Println(s)
}
}
20. Generator Pattern: a Function that Returns a Lazy Channel
Write a function that returns a channel producing a stream of computed values on demand, one at a time, only continuing work once the consumer asks for the next value.
package main
import "fmt"
// squares returns a channel that lazily produces the squares of 1, 2, 3, ...
// It only computes and sends the next value once the consumer asks for it.
func squares(done <-chan struct{}) <-chan int {
// Unbuffered: the goroutine below can only get one value ahead of the
// consumer, since a send blocks until something receives it. That's what
// makes generation "lazy" instead of racing ahead to produce every value.
out := make(chan int)
go func() {
defer close(out)
for n := 1; ; n++ {
// Without the done case, this send would block forever once main stops
// reading from gen, leaking this goroutine for the rest of the program.
select {
case out <- n * n:
// value delivered, generator waits for next request
case <-done:
// A closed channel is always immediately ready to receive from, so once
// close(done) runs, this case wins the select and the generator exits.
return // consumer stopped listening; stop generating
}
}
}()
return out
}
func main() {
done := make(chan struct{})
gen := squares(done)
// Only pull the first 5 values on demand; the generator produces them
// one at a time and would keep going forever if we kept asking.
for i := 0; i < 5; i++ {
v := <-gen
fmt.Printf("requested value %d: %d\n", i+1, v)
}
close(done) // tell the generator to stop producing further values
fmt.Println("consumer: done pulling values, generator signaled to stop")
}
21. Merge Exactly Two Channels with a select Loop
Combine values from two input channels into one output stream using a select loop that keeps running until both inputs are closed, nil-ing out each channel once it's drained so it stops being selected.
package main
import (
"fmt"
"time"
)
func source(name string, delays []time.Duration) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for i, d := range delays {
time.Sleep(d)
out <- fmt.Sprintf("%s-%d", name, i+1)
}
}()
return out
}
// mergeTwo reads from both a and b until both are closed, preserving
// whichever one becomes ready at each step.
func mergeTwo(a, b <-chan string) <-chan string {
// A single goroutine drives this merge, so values only ever go into out
// one at a time, in the same order select picked them up in below.
out := make(chan string)
go func() {
defer close(out)
// Loop until both source channels are nilled out below; a nil channel
// in a select case blocks forever, so once one source is drained the
// loop naturally keeps selecting only the other one.
for a != nil || b != nil {
select {
case v, ok := <-a:
if !ok {
// Receiving from a closed channel never blocks and always succeeds,
// so without nilling a out here, this case would fire on every loop
// iteration going forward and spam zero-value strings into out.
a = nil // stop selecting a closed channel
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil // same trick: remove b from consideration once it's drained
continue
}
out <- v
}
}
}()
return out
}
func main() {
// Delays are staggered so each value becomes ready at a distinct,
// well-separated moment, giving a deterministic merge order.
a := source("A", []time.Duration{20 * time.Millisecond, 40 * time.Millisecond, 40 * time.Millisecond})
b := source("B", []time.Duration{40 * time.Millisecond, 40 * time.Millisecond})
for v := range mergeTwo(a, b) {
fmt.Println("received:", v)
}
fmt.Println("both channels closed, merge complete")
}
22. Avoid Deadlock with Ordered Lock Acquisition
Several goroutines each need to hold two shared mutexes at once. Acquiring them in whatever order each goroutine happens to need risks a deadlock; the fix is to always acquire the two locks in the same fixed global order.
package main
import (
"fmt"
"sync"
)
type Account struct {
id int
mu sync.Mutex
balance int
}
// transfer moves amount from one account to another. If two goroutines ran
// transfers in opposite directions (A->B and B->A) and each locked "from"
// then "to", they could deadlock (each holding one lock, waiting on the
// other). The fix: always acquire the two locks in a fixed global order
// (lowest account id first), regardless of transfer direction.
func transfer(from, to *Account, amount int) {
// Reorder by id so a transfer A->B and a concurrent transfer B->A both
// lock the same account first. Whichever goroutine gets there first fully
// acquires both locks before the other can grab even one, so no goroutine
// ever waits on a lock held by something that is itself waiting on it.
first, second := from, to
if first.id > second.id {
first, second = second, first
}
first.mu.Lock()
defer first.mu.Unlock()
second.mu.Lock()
defer second.mu.Unlock()
from.balance -= amount
to.balance += amount
}
func main() {
a := &Account{id: 1, balance: 500}
b := &Account{id: 2, balance: 500}
var wg sync.WaitGroup
transfers := []struct {
from, to *Account
amount int
}{
{a, b, 100}, // A -> B
{b, a, 50}, // B -> A
{a, b, 30}, // A -> B
{b, a, 80}, // B -> A
}
for _, t := range transfers {
wg.Add(1)
go func(from, to *Account, amount int) {
defer wg.Done()
transfer(from, to, amount)
}(t.from, t.to, t.amount)
}
// If transfer didn't reorder its locks, this mix of A->B and B->A transfers
// running concurrently would be exactly the pattern that can deadlock.
wg.Wait()
fmt.Println("all transfers completed without deadlock (locks always acquired lowest-id first)")
// the four transfers can run in any order relative to each other, but +=
// and -= are commutative, so the final balances come out the same
// regardless of that order -- it's only the deadlock risk, not the result,
// that depends on lock ordering here
fmt.Printf("account %d balance: %d\n", a.id, a.balance)
fmt.Printf("account %d balance: %d\n", b.id, b.balance)
}
23. Token Bucket Rate Limiting with a Refilled Channel
Implement a token bucket by hand: a buffered channel holds tokens, a background goroutine refills it on a ticker (dropping the token if the bucket is already full), and consumers must take a token before proceeding.
package main
import (
"fmt"
"time"
)
func main() {
const bucketSize = 3
const numRequests = 5
// A buffered channel doubles as the token bucket itself: its capacity is
// the max number of tokens that can ever be outstanding at once, and
// sends/receives on it are already safe across goroutines with no extra
// locking needed.
bucket := make(chan struct{}, bucketSize)
for i := 0; i < bucketSize; i++ {
bucket <- struct{}{} // start with a full bucket
}
stopRefill := make(chan struct{})
go func() {
// A ticker fires on a fixed 10ms schedule, and pairing it with select lets
// this goroutine keep listening for the stop signal between ticks instead
// of being stuck inside a blocking time.Sleep with no way to exit early.
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// The inner select's default case makes this a non-blocking send: if the
// bucket is already at bucketSize tokens, this refill is simply dropped
// instead of the whole goroutine stalling on a full channel. That's what
// actually caps the bucket at bucketSize rather than letting tokens pile
// up unbounded.
select {
case bucket <- struct{}{}: // add a token if there's room
default: // bucket full, drop the token
}
case <-stopRefill:
return // closing stopRefill below lets this goroutine exit instead of leaking
}
}
}()
for i := 1; i <= numRequests; i++ {
// Because bucket is buffered, this only blocks once it's genuinely empty:
// requests 1-3 drain the initial fill instantly, but requests 4 and 5 each
// have to wait for the ticker to deposit another token - that wait is the
// rate limiting.
<-bucket // blocks until a token is available
fmt.Printf("request %d: granted a token\n", i)
}
close(stopRefill)
fmt.Println("token bucket: all requests served, first 3 used the initial fill, the rest waited for refills")
}
24. First Result Wins: Racing Redundant Goroutines
Launch several goroutines that redundantly compute or fetch the same answer, then use select to take whichever one responds first and discard the rest.
package main
import (
"fmt"
"time"
)
// query simulates asking one of several redundant sources for the same
// answer. Each has a buffered result channel so a "losing" goroutine can
// still send its value without blocking forever (and leaking).
func query(name string, delay time.Duration) <-chan string {
result := make(chan string, 1)
go func() {
time.Sleep(delay)
result <- fmt.Sprintf("%s: answer ready", name)
}()
return result
}
func main() {
// Query three redundant sources at once; take whichever answers first.
mirrorA := query("mirror-A", 10*time.Millisecond)
mirrorB := query("mirror-B", 50*time.Millisecond)
mirrorC := query("mirror-C", 90*time.Millisecond)
// select would pick fairly among cases that are ready at the same instant,
// but the delays above are staggered widely enough that mirrorA is reliably
// the only one ready when this runs, making the winner deterministic here
var winner string
select {
case winner = <-mirrorA:
case winner = <-mirrorB:
case winner = <-mirrorC:
}
fmt.Println("first result wins:", winner)
// mirrorB and mirrorC's goroutines are still running at this point and will
// eventually send too, but because their result channels are buffered with
// room for that one value, those sends complete immediately instead of
// blocking forever with no receiver - the goroutines simply exit and their
// unread values are garbage collected along with the channels.
fmt.Println("the other two responses are discarded once a winner is chosen")
}
1. Two Sum
Given a slice of integers and a target, return the indices of the two numbers that add up to the target, in a single O(n) pass using a map.
package main
import "fmt"
// twoSum returns the indices of the two numbers in nums that add up to target.
// It uses a map to record each value's index, giving O(n) time instead of O(n^2).
func twoSum(nums []int, target int) (int, int, bool) {
seen := make(map[int]int) // value -> index
for i, n := range nums {
complement := target - n
// check before inserting the current number: this guarantees j < i and
// prevents pairing an element with itself when target is exactly 2*n
if j, ok := seen[complement]; ok {
return j, i, true
}
seen[n] = i
}
return 0, 0, false
}
func main() {
nums := []int{2, 7, 11, 15}
target := 9
i, j, ok := twoSum(nums, target)
fmt.Printf("nums=%v target=%d\n", nums, target)
if ok {
fmt.Printf("indices: [%d, %d] -> values %d + %d = %d\n", i, j, nums[i], nums[j], target)
}
nums2 := []int{3, 2, 4}
target2 := 6
i2, j2, ok2 := twoSum(nums2, target2)
fmt.Printf("\nnums=%v target=%d\n", nums2, target2)
if ok2 {
fmt.Printf("indices: [%d, %d] -> values %d + %d = %d\n", i2, j2, nums2[i2], nums2[j2], target2)
}
nums3 := []int{1, 2, 3}
target3 := 100
_, _, ok3 := twoSum(nums3, target3)
fmt.Printf("\nnums=%v target=%d\n", nums3, target3)
fmt.Printf("found pair: %v\n", ok3)
}
2. Reverse a String (Unicode-safe)
Reverse a string by its runes rather than its bytes, so multi-byte UTF-8 characters like accented letters or CJK text aren't corrupted.
package main
import "fmt"
// reverseString reverses a string rune by rune so multi-byte UTF-8
// characters (like accented letters or emoji) are not corrupted.
func reverseString(s string) string {
runes := []rune(s)
// walk from both ends toward the middle; stopping at i < j means an odd-length
// string's middle rune is left in place instead of being swapped with itself
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func main() {
inputs := []string{"Hello, Go!", "Golang", "héllo wörld", "日本語"}
for _, s := range inputs {
fmt.Printf("input: %q\n", s)
fmt.Printf("output: %q\n\n", reverseString(s))
}
}
3. Valid Palindrome
Check whether a string is a palindrome, ignoring case and skipping any character that isn't a letter or digit (so punctuation and spaces don't break the check).
package main
import (
"fmt"
"unicode"
)
// isPalindrome reports whether s reads the same forwards and backwards,
// ignoring case and skipping any character that isn't a letter or digit.
func isPalindrome(s string) bool {
runes := []rune(s)
i, j := 0, len(runes)-1
for i < j {
// advance i past punctuation/spaces on its own; a continue here re-checks
// i < j without touching j, so the two sides skip independently
if !unicode.IsLetter(runes[i]) && !unicode.IsDigit(runes[i]) {
i++
continue
}
// same skip, mirrored for j, so unequal amounts of punctuation on each
// side (e.g. "a," vs "a") don't throw off the alignment of the comparison
if !unicode.IsLetter(runes[j]) && !unicode.IsDigit(runes[j]) {
j--
continue
}
if unicode.ToLower(runes[i]) != unicode.ToLower(runes[j]) {
return false
}
i++
j--
}
return true
}
func main() {
inputs := []string{
"A man, a plan, a canal: Panama",
"race a car",
"Was it a car or a cat I saw?",
"12321",
}
for _, s := range inputs {
fmt.Printf("input: %q -> palindrome: %v\n", s, isPalindrome(s))
}
}
4. Valid Anagram
Determine whether two strings are anagrams of each other — the same letters rearranged, ignoring case and spaces — by counting rune frequencies.
package main
import (
"fmt"
"unicode"
)
// isAnagram reports whether a and b contain exactly the same letters
// (case-insensitive), just rearranged, by counting rune frequencies.
func isAnagram(a, b string) bool {
counts := make(map[rune]int)
for _, r := range a {
if unicode.IsSpace(r) {
continue
}
counts[unicode.ToLower(r)]++
}
// reuse the same map and decrement for b instead of building a second map:
// if a and b are anagrams, every letter's count nets back to exactly zero
for _, r := range b {
if unicode.IsSpace(r) {
continue
}
counts[unicode.ToLower(r)]--
}
// any surviving nonzero count means that letter appeared a different
// number of times in a than in b
for _, c := range counts {
if c != 0 {
return false
}
}
return true
}
func main() {
pairs := [][2]string{
{"listen", "silent"},
{"Astronomer", "Moon starer"},
{"hello", "world"},
}
for _, p := range pairs {
fmt.Printf("%q vs %q -> anagram: %v\n", p[0], p[1], isAnagram(p[0], p[1]))
}
}
5. FizzBuzz
The classic warm-up: print numbers 1 to 20, but "Fizz" for multiples of 3, "Buzz" for multiples of 5, and "FizzBuzz" for multiples of both — shown with idiomatic Go switch/case.
package main
import "fmt"
// fizzBuzz returns the classic FizzBuzz label for n:
// "Fizz" for multiples of 3, "Buzz" for multiples of 5,
// "FizzBuzz" for multiples of both, otherwise the number itself.
func fizzBuzz(n int) string {
switch {
// the multiple-of-15 case must be checked first: every multiple of 15 is
// also a multiple of 3 and 5, so checking those individually first would
// always return early with just "Fizz" or "Buzz" and never "FizzBuzz"
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return fmt.Sprintf("%d", n)
}
}
func main() {
for i := 1; i <= 20; i++ {
fmt.Println(fizzBuzz(i))
}
}
6. Binary Search
Find the index of a target value in a sorted slice in O(log n) time by repeatedly halving the search range.
package main
import "fmt"
// binarySearch returns the index of target in a sorted slice, or -1 if
// it isn't present. It halves the search range each step: O(log n).
func binarySearch(sorted []int, target int) int {
lo, hi := 0, len(sorted)-1
for lo <= hi {
// lo + (hi-lo)/2 instead of (lo+hi)/2: the latter can overflow int when
// lo and hi are both large, even though neither alone is out of range
mid := lo + (hi-lo)/2
switch {
case sorted[mid] == target:
return mid
case sorted[mid] < target:
// target is to the right; mid is excluded since it's already ruled out
lo = mid + 1
default:
// target is to the left; same reasoning, mid is excluded going down
hi = mid - 1
}
}
return -1
}
func main() {
sorted := []int{1, 3, 5, 7, 9, 11, 13, 17, 19}
targets := []int{7, 19, 1, 4}
for _, t := range targets {
idx := binarySearch(sorted, t)
fmt.Printf("searching for %d in %v -> index %d\n", t, sorted, idx)
}
}
7. First Duplicate in a Slice
Scan a slice left to right and return the first value that has already appeared earlier, using a set for an O(n) solution.
package main
import "fmt"
// firstDuplicate scans nums left to right and returns the first value
// that has already been seen earlier in the slice, using a set for O(n).
func firstDuplicate(nums []int) (int, bool) {
// empty struct{} as the value costs no extra memory; the map is used purely as a set
seen := make(map[int]struct{})
for _, n := range nums {
// check membership before recording n, so a value doesn't count as its own duplicate
if _, ok := seen[n]; ok {
return n, true
}
seen[n] = struct{}{}
}
// reached the end with every value unique
return 0, false
}
func main() {
cases := [][]int{
{2, 1, 3, 5, 3, 2},
{1, 2, 3, 4},
{7, 7, 1, 2},
}
for _, nums := range cases {
dup, ok := firstDuplicate(nums)
fmt.Printf("nums=%v -> ", nums)
if ok {
fmt.Printf("first duplicate: %d\n", dup)
} else {
fmt.Println("no duplicate found")
}
}
}
8. Merge Two Sorted Slices
Merge two already-sorted integer slices into a single sorted slice in O(n+m) time using the two-pointer technique.
package main
import "fmt"
// mergeSorted merges two already-sorted slices into one sorted slice
// in O(n+m) time by walking both with two pointers.
func mergeSorted(a, b []int) []int {
// pre-size the backing array to the combined length so append never reallocates
merged := make([]int, 0, len(a)+len(b))
i, j := 0, 0
// advance whichever pointer holds the smaller front element, so merged stays sorted
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
merged = append(merged, a[i])
i++
} else {
merged = append(merged, b[j])
j++
}
}
// one slice is exhausted by now; the other's remaining tail is already sorted,
// so append it wholesale instead of comparing element by element
merged = append(merged, a[i:]...)
merged = append(merged, b[j:]...)
return merged
}
func main() {
a := []int{1, 3, 5, 7}
b := []int{2, 4, 6, 8, 10}
fmt.Printf("a=%v\nb=%v\nmerged=%v\n\n", a, b, mergeSorted(a, b))
c := []int{}
d := []int{-3, -1, 0, 2}
fmt.Printf("a=%v\nb=%v\nmerged=%v\n", c, d, mergeSorted(c, d))
}
9. Longest Substring Without Repeating Characters
Find the length and text of the longest run of characters in a string with no repeats, using a sliding window and a map of each rune's last-seen index.
package main
import "fmt"
// longestUniqueSubstring returns the length and text of the longest run
// of characters in s with no repeats, using a sliding window plus a map
// of each rune's most recent index to jump the window start forward.
func longestUniqueSubstring(s string) (int, string) {
runes := []rune(s)
lastSeen := make(map[rune]int)
best, bestStart := 0, 0
start := 0
for i, r := range runes {
// idx >= start matters: a stale sighting of r from before the current window
// started is irrelevant and must not push start forward again
if idx, ok := lastSeen[r]; ok && idx >= start {
start = idx + 1
}
// record/overwrite the most recent index of r regardless, so future repeats see it
lastSeen[r] = i
// current window [start, i] is unique by construction; keep it if it's the longest so far
if length := i - start + 1; length > best {
best = length
bestStart = start
}
}
return best, string(runes[bestStart : bestStart+best])
}
func main() {
inputs := []string{"abcabcbb", "bbbbb", "pwwkew", "go gopher"}
for _, s := range inputs {
n, sub := longestUniqueSubstring(s)
fmt.Printf("input: %q -> longest unique substring: %q (length %d)\n", s, sub, n)
}
}
10. Valid Parentheses
Check whether every bracket in a string is properly opened and closed in the right order — the classic stack-based balanced-brackets problem.
package main
import "fmt"
// isBalanced reports whether every bracket in s is properly opened and
// closed in the right order, using a stack to track open brackets.
func isBalanced(s string) bool {
// maps each closing bracket to the opener it must match
pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}
// the most recently opened, still-unclosed bracket is always on top,
// which is exactly what a stack gives us in O(1) per operation
var stack []rune
for _, r := range s {
switch r {
case '(', '[', '{':
stack = append(stack, r)
case ')', ']', '}':
// invalid if there's nothing open to close, or the top of the stack
// isn't the matching opener (wrong type or wrong order)
if len(stack) == 0 || stack[len(stack)-1] != pairs[r] {
return false
}
stack = stack[:len(stack)-1]
}
}
// a leftover open bracket with nothing closing it is also unbalanced
return len(stack) == 0
}
func main() {
inputs := []string{"()[]{}", "([{}])", "(]", "([)]", "{[()()]}"}
for _, s := range inputs {
fmt.Printf("input: %-10q -> balanced: %v\n", s, isBalanced(s))
}
}
11. Fibonacci: Naive Recursion vs. Memoization
Compute Fibonacci numbers two ways — plain recursion, which recomputes the same subproblems exponentially (O(2^n)), and a memoized version that caches results for O(n).
package main
import "fmt"
// fibRecursive computes the n-th Fibonacci number the naive way, recursing
// on both smaller subproblems. It recomputes the same values many times,
// so it's exponential time: O(2^n).
func fibRecursive(n int) int {
// base cases fib(0)=0 and fib(1)=1 stop the recursion
if n < 2 {
return n
}
// each call branches into two more calls with no memory of work already done,
// so the same smaller subproblems (e.g. fib(3)) get recomputed many times over
return fibRecursive(n-1) + fibRecursive(n-2)
}
// fibMemo computes the n-th Fibonacci number using a cache of already
// solved subproblems, so each value is computed only once: O(n) time.
func fibMemo(n int, cache map[int]int) int {
if n < 2 {
return n
}
// if this subproblem was already solved on an earlier branch, reuse it
// instead of paying for the recursion again
if v, ok := cache[n]; ok {
return v
}
result := fibMemo(n-1, cache) + fibMemo(n-2, cache)
// save the result before returning so every future call for n is O(1)
cache[n] = result
return result
}
func main() {
fmt.Println("naive recursive fibonacci:")
for i := 0; i <= 10; i++ {
fmt.Printf("fib(%d) = %d\n", i, fibRecursive(i))
}
fmt.Println("\nmemoized fibonacci:")
// one cache shared across every call in this loop, so it keeps growing instead
// of being rebuilt from scratch each time
cache := make(map[int]int)
for i := 0; i <= 10; i++ {
fmt.Printf("fib(%d) = %d\n", i, fibMemo(i, cache))
}
fmt.Println("\nmemoization makes larger values practical, e.g.:")
// fib(40) would take billions of calls with fibRecursive; reusing the
// already-populated cache makes it near-instant here
fmt.Printf("fib(40) = %d\n", fibMemo(40, cache))
}
12. Quicksort
Implement quicksort from scratch on a slice of ints: pick a pivot, partition around it with two pointers, then recursively sort each side for average O(n log n).
package main
import "fmt"
// quicksort sorts nums in place by picking a pivot, partitioning the
// slice around it, then recursively sorting each side: average O(n log n).
func quicksort(nums []int) {
// 0 or 1 elements are trivially sorted; this also ends the recursion
if len(nums) < 2 {
return
}
// the middle element avoids the worst-case O(n^2) behavior that picking
// the first or last element would cause on already-sorted input
pivot := nums[len(nums)/2]
left, right := 0, len(nums)-1
// Hoare partition: walk left up and right down past elements already on
// the correct side of the pivot, swapping the ones that are out of place
for left <= right {
for nums[left] < pivot {
left++
}
for nums[right] > pivot {
right--
}
if left <= right {
nums[left], nums[right] = nums[right], nums[left]
// move past the pair just swapped so it isn't re-examined
left++
right--
}
}
// right and left have crossed, splitting nums into two partitions around
// the pivot's final resting place; recursively sort each independently
quicksort(nums[:right+1])
quicksort(nums[left:])
}
func main() {
nums := []int{8, 3, 1, 7, 0, 10, 2}
fmt.Printf("before: %v\n", nums)
quicksort(nums)
fmt.Printf("after: %v\n", nums)
nums2 := []int{5, 5, 5, -1, 3}
fmt.Printf("\nbefore: %v\n", nums2)
quicksort(nums2)
fmt.Printf("after: %v\n", nums2)
}
13. Maximum Subarray Sum (Kadane's Algorithm)
Given a slice of integers, find the largest possible sum of any contiguous, non-empty subarray, in a single O(n) pass using Kadane's algorithm.
package main
import "fmt"
// maxSubArray returns the largest sum of any contiguous, non-empty subarray
// of nums, using Kadane's algorithm in O(n) time and O(1) extra space.
func maxSubArray(nums []int) int {
best := nums[0]
cur := nums[0]
for i := 1; i < len(nums); i++ {
n := nums[i]
// a negative running sum can only drag down any future subarray,
// so it's never worth carrying forward - start fresh from n instead
if cur < 0 {
cur = n
} else {
cur += n
}
// best tracks the max cur seen at any point, since the optimal
// subarray must end at some index i and cur is the best sum ending there
if cur > best {
best = cur
}
}
return best
}
func main() {
inputs := [][]int{
{-2, 1, -3, 4, -1, 2, 1, -5, 4},
{5, 4, -1, 7, 8},
{-3, -1, -8, -2},
}
for _, nums := range inputs {
fmt.Printf("nums=%v -> max subarray sum=%d\n", nums, maxSubArray(nums))
}
}
14. Group Anagrams
Given a slice of strings, group the words that are anagrams of each other into clusters, by sorting each word's runes into a canonical bucket key.
package main
import (
"fmt"
"sort"
)
// groupAnagrams clusters strings that are anagrams of each other by sorting
// each word's runes into a canonical key and bucketing on that key.
func groupAnagrams(words []string) [][]string {
groups := make(map[string][]string)
var order []string
for _, w := range words {
// any two anagrams produce the same sorted rune sequence, so that
// sorted form is a canonical key every member of the group shares
runes := []rune(w)
sort.Slice(runes, func(i, j int) bool { return runes[i] < runes[j] })
key := string(runes)
// record each key only the first time it's seen, so groups come
// out in first-appearance order even though map iteration isn't ordered
if _, ok := groups[key]; !ok {
order = append(order, key)
}
groups[key] = append(groups[key], w)
}
result := make([][]string, 0, len(order))
for _, key := range order {
result = append(result, groups[key])
}
return result
}
func main() {
words := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
fmt.Printf("words=%v\n", words)
for _, group := range groupAnagrams(words) {
fmt.Printf("group: %v\n", group)
}
}
15. Longest Common Prefix
Given a slice of strings, find the longest prefix string shared by every word, comparing column by column across all words at once.
package main
import "fmt"
// longestCommonPrefix returns the longest prefix shared by every string in
// strs, comparing column by column across all words at once.
func longestCommonPrefix(strs []string) string {
if len(strs) == 0 {
return ""
}
// start assuming the whole first word is the prefix, then shrink it
// down as each subsequent word disagrees with it
prefix := strs[0]
for _, s := range strs[1:] {
i := 0
// walk both strings together and stop at the first mismatching
// column (or the end of the shorter one) - everything before that
// point is still common to every word seen so far
for i < len(prefix) && i < len(s) && prefix[i] == s[i] {
i++
}
prefix = prefix[:i]
// once the prefix is empty it can't shrink any further, so there's
// no point checking the remaining words
if prefix == "" {
break
}
}
return prefix
}
func main() {
cases := [][]string{
{"flower", "flow", "flight"},
{"dog", "racecar", "car"},
{"interspecies", "interstellar", "interstate"},
}
for _, strs := range cases {
fmt.Printf("strs=%v -> longest common prefix=%q\n", strs, longestCommonPrefix(strs))
}
}
16. Kth Largest Element in a Slice
Find the k-th largest value in an unsorted slice of integers using a bounded min-heap of size k, built with the standard library's container/heap.
package main
import (
"container/heap"
"fmt"
)
// minHeap is an int min-heap; we keep it capped at size k so its root is
// always the k-th largest value seen so far.
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 interface{}) { *h = append(*h, x.(int)) }
func (h *minHeap) Pop() interface{} {
old := *h
n := len(old)
v := old[n-1]
*h = old[:n-1]
return v
}
// kthLargest returns the k-th largest element of nums using a bounded
// min-heap of size k, giving O(n log k) time.
func kthLargest(nums []int, k int) int {
h := &minHeap{}
heap.Init(h)
for _, n := range nums {
heap.Push(h, n)
// once the heap holds more than k elements, the smallest of them
// can no longer be among the k largest overall, so evict it
if h.Len() > k {
heap.Pop(h)
}
}
// after processing every element, the heap holds exactly the k
// largest values, and its root is the smallest of those - i.e. the k-th largest
return (*h)[0]
}
func main() {
nums := []int{3, 2, 1, 5, 6, 4}
for _, k := range []int{2, 4} {
fmt.Printf("nums=%v, k=%d -> %d-th largest=%d\n", nums, k, k, kthLargest(nums, k))
}
nums2 := []int{3, 2, 3, 1, 2, 4, 5, 5, 6}
k := 4
fmt.Printf("nums=%v, k=%d -> %d-th largest=%d\n", nums2, k, k, kthLargest(nums2, k))
}
17. Rotate an Array by K Positions
Rotate a slice of integers to the right by k positions in place, using the reverse-three-times trick instead of an auxiliary array.
package main
import "fmt"
// reverse flips the elements of s in place between indices lo and hi.
func reverse(s []int, lo, hi int) {
for lo < hi {
s[lo], s[hi] = s[hi], s[lo]
lo++
hi--
}
}
// rotate shifts nums to the right by k positions in place, using the
// reverse-three-times trick: reverse the whole slice, then reverse each part.
func rotate(nums []int, k int) {
n := len(nums)
if n == 0 {
return
}
// k can exceed n (rotating by n is a no-op), so reduce it to its
// effective value in [0, n) before using it as an index
k %= n
// reversing the whole slice puts every element in its final half but
// backwards within each half; reversing the two halves individually
// then un-reverses each one back into the correct order
reverse(nums, 0, n-1)
reverse(nums, 0, k-1)
reverse(nums, k, n-1)
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7}
k := 3
fmt.Printf("before=%v, k=%d\n", nums, k)
rotate(nums, k)
fmt.Printf("after=%v\n", nums)
nums2 := []int{-1, -100, 3, 99}
k2 := 2
fmt.Printf("before=%v, k=%d\n", nums2, k2)
rotate(nums2, k2)
fmt.Printf("after=%v\n", nums2)
nums3 := []int{1, 2, 3}
k3 := 10
fmt.Printf("before=%v, k=%d\n", nums3, k3)
rotate(nums3, k3)
fmt.Printf("after=%v\n", nums3)
}
18. Container With Most Water
Given a slice of non-negative heights representing vertical lines, find the two lines that together with the x-axis form the container holding the most water, using a two-pointer scan.
package main
import "fmt"
// maxArea finds the two lines in height that, together with the x-axis,
// form the container holding the most water, using the two-pointer method.
func maxArea(height []int) int {
// start with the widest possible container and narrow it, since width
// only shrinks from here - any better area has to come from a taller side
lo, hi := 0, len(height)-1
best := 0
for lo < hi {
// the container's height is capped by its shorter wall
h := height[lo]
if height[hi] < h {
h = height[hi]
}
area := h * (hi - lo)
if area > best {
best = area
}
// move whichever side is shorter: keeping it in place can never beat
// the current area (width shrinks, height stays capped by the same wall),
// so moving it is the only way to possibly find a taller pair
if height[lo] < height[hi] {
lo++
} else {
hi--
}
}
return best
}
func main() {
cases := [][]int{
{1, 8, 6, 2, 5, 4, 8, 3, 7},
{1, 1},
{4, 3, 2, 1, 4},
}
for _, height := range cases {
fmt.Printf("height=%v -> max water area=%d\n", height, maxArea(height))
}
}
19. Move Zeroes
Given a slice of integers, move every zero to the end in place while preserving the relative order of the non-zero elements, using a single write pointer.
package main
import "fmt"
// moveZeroes shifts every zero in nums to the end, in place, while keeping
// the relative order of the non-zero elements, using a single write pointer.
func moveZeroes(nums []int) {
// write marks the boundary: everything before it is the non-zero
// elements packed so far, in their original relative order.
write := 0
for read := 0; read < len(nums); read++ {
if nums[read] != 0 {
// swapping (rather than just assigning) puts whatever was at
// write - a zero already accounted for, or nums[write] itself
// when read==write - into read's old slot, so no extra buffer
// or second pass is needed to shift the zeros down.
nums[write], nums[read] = nums[read], nums[write]
write++
}
}
}
func main() {
nums := []int{0, 1, 0, 3, 12}
fmt.Printf("before=%v\n", nums)
moveZeroes(nums)
fmt.Printf("after=%v\n", nums)
nums2 := []int{0, 0, 1}
fmt.Printf("before=%v\n", nums2)
moveZeroes(nums2)
fmt.Printf("after=%v\n", nums2)
nums3 := []int{4, 2, 4, 0, 0, 3, 0, 5, 1, 0}
fmt.Printf("before=%v\n", nums3)
moveZeroes(nums3)
fmt.Printf("after=%v\n", nums3)
}
20. String Compression (Run-Length Encoding)
Compress a string by replacing each run of a repeated character with the character followed by its run length, e.g. "aaabbc" becomes "a3b2c1".
package main
import (
"fmt"
"strconv"
"strings"
)
// compress run-length-encodes s, replacing each run of a repeated character
// with the character followed by its run length (e.g. "aaabbc" -> "a3b2c1").
func compress(s string) string {
if s == "" {
return ""
}
var b strings.Builder
// decode to runes first so multi-byte characters count as one character
// each, not as however many UTF-8 bytes they occupy.
runeStr := []rune(s)
count := 1
// i runs one past the last index on purpose: that out-of-range step is
// what forces the final run to be flushed, since otherwise the loop
// would end mid-run with count never written out.
for i := 1; i <= len(runeStr); i++ {
if i < len(runeStr) && runeStr[i] == runeStr[i-1] {
count++
continue
}
// runeStr[i-1] is the last character of the run that just ended
// (or the run-end sentinel step at i==len(runeStr)); flush it with
// its accumulated count before starting the next run.
b.WriteRune(runeStr[i-1])
b.WriteString(strconv.Itoa(count))
count = 1
}
return b.String()
}
func main() {
inputs := []string{"aaabbc", "abcd", "aabbccddeeee", "wwwwwwwwwwwwx"}
for _, s := range inputs {
fmt.Printf("input=%q -> compressed=%q\n", s, compress(s))
}
}
21. Check if a String Is a Rotation of Another
Determine whether one string is a rotation of another (e.g. "erbottlewat" is a rotation of "waterbottle") using the classic trick that any rotation of s1 is a substring of s1+s1.
package main
import (
"fmt"
"strings"
)
// isRotation reports whether s2 is a rotation of s1 (e.g. "erbottlewat" is a
// rotation of "waterbottle"). The trick: any rotation of s1 is a substring
// of s1+s1.
func isRotation(s1, s2 string) bool {
// a rotation must have exactly the same length; without this check a
// shorter s2 could false-positive as "contained" in s1+s1.
if len(s1) != len(s2) {
return false
}
// concatenating s1 with itself lines up every possible rotation point
// as a contiguous substring, so a single Contains check covers all of
// them without trying each rotation offset individually.
return strings.Contains(s1+s1, s2)
}
func main() {
cases := []struct{ a, b string }{
{"waterbottle", "erbottlewat"},
{"hello", "llohe"},
{"hello", "helol"},
{"abc", "abcd"},
}
for _, c := range cases {
fmt.Printf("s1=%q, s2=%q -> is rotation=%t\n", c.a, c.b, isRotation(c.a, c.b))
}
}
22. Merge Overlapping Intervals
Given a slice of [start, end] interval pairs, merge all overlapping intervals and return the minimal set of non-overlapping intervals that covers the same ranges.
package main
import (
"fmt"
"sort"
)
// merge sorts intervals by start and merges any that overlap, returning the
// minimal set of non-overlapping intervals that covers the same ranges.
func merge(intervals [][2]int) [][2]int {
if len(intervals) == 0 {
return nil
}
// copy first so the caller's slice isn't reordered as a side effect of
// sorting it in place.
sorted := make([][2]int, len(intervals))
copy(sorted, intervals)
// sorting by start turns "do any two intervals overlap" into "does each
// interval overlap only its immediate neighbor", since once ordered by
// start, an overlap can only ever involve the most recently merged one.
sort.Slice(sorted, func(i, j int) bool { return sorted[i][0] < sorted[j][0] })
result := [][2]int{sorted[0]}
for _, cur := range sorted[1:] {
// take the address of the slice element so extending it in place
// (below) updates result directly, without a separate lookup/write.
last := &result[len(result)-1]
if cur[0] <= last[1] {
// cur starts inside (or right at) last's range, so they overlap;
// only grow the end if cur reaches further, since cur could be
// fully contained within last (e.g. [1,4] then [2,3]).
if cur[1] > last[1] {
last[1] = cur[1]
}
} else {
// cur starts after last ends, so it can't overlap anything seen
// so far (thanks to the sort) - it becomes its own interval.
result = append(result, cur)
}
}
return result
}
func main() {
cases := [][][2]int{
{{1, 3}, {2, 6}, {8, 10}, {15, 18}},
{{1, 4}, {4, 5}},
{{1, 4}, {0, 4}},
{{1, 4}, {2, 3}},
}
for _, intervals := range cases {
fmt.Printf("intervals=%v -> merged=%v\n", intervals, merge(intervals))
}
}
23. Word Frequency Counter
Count how many times each word occurs in a piece of text, case-insensitively and ignoring punctuation, then report the results ordered by count.
package main
import (
"fmt"
"sort"
"strings"
"unicode"
)
// wordFrequency splits text into lowercase words (stripping punctuation) and
// counts how many times each word appears.
func wordFrequency(text string) map[string]int {
// treat any rune that's neither a letter nor a digit as a separator, so
// FieldsFunc splits on punctuation and whitespace alike in one pass
// without a separate strip-punctuation step.
fields := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
})
// a map gives O(1) increment per word instead of a linear scan for each
// occurrence to find its existing count.
counts := make(map[string]int)
for _, w := range fields {
counts[w]++
}
return counts
}
// sortedByCount returns words ordered by descending count, then alphabetically
// to keep output deterministic.
func sortedByCount(counts map[string]int) []string {
// Go's map iteration order is randomized, so the keys must be collected
// and explicitly sorted to get any stable, repeatable output.
words := make([]string, 0, len(counts))
for w := range counts {
words = append(words, w)
}
// primary key: count descending. Ties fall back to alphabetical order
// so words with equal counts still print in a fixed, reproducible order.
sort.Slice(words, func(i, j int) bool {
if counts[words[i]] != counts[words[j]] {
return counts[words[i]] > counts[words[j]]
}
return words[i] < words[j]
})
return words
}
func main() {
text := "The quick brown fox jumps over the lazy dog. The dog barks, but the fox runs away!"
fmt.Printf("text=%q\n", text)
counts := wordFrequency(text)
for _, w := range sortedByCount(counts) {
fmt.Printf("%s: %d\n", w, counts[w])
}
}
24. Number of Islands
Given a 2D grid of '1' (land) and '0' (water) cells, count the number of connected land regions, where cells are connected horizontally or vertically, using BFS flood-fill.
package main
import "fmt"
// numIslands counts the connected regions of '1' (land) in grid, where a
// region is any set of land cells connected horizontally or vertically.
// It flood-fills each newly found island with BFS so it is never re-counted.
func numIslands(grid [][]byte) int {
if len(grid) == 0 {
return 0
}
rows, cols := len(grid), len(grid[0])
// visited is a parallel grid, separate from the input, marking every
// land cell already assigned to an island; without it the outer scan
// below would walk into the same island repeatedly and over-count it,
// and the BFS itself could revisit a cell and loop forever.
visited := make([][]bool, rows)
for i := range visited {
visited[i] = make([]bool, cols)
}
type point struct{ r, c int }
// bfs claims every land cell reachable from (sr, sc) by 4-directional
// steps, marking each one visited the moment it's enqueued (not when
// it's dequeued) so the same cell can never be added to the queue twice.
bfs := func(sr, sc int) {
queue := []point{{sr, sc}}
visited[sr][sc] = true
for len(queue) > 0 {
// pop from the front - this is what makes it a breadth-first
// (layer-by-layer) search rather than depth-first; either order
// finds the same island, but BFS avoids deep recursion on large
// islands.
p := queue[0]
queue = queue[1:]
// the four orthogonal neighbors; islands are defined by
// horizontal/vertical adjacency only, so diagonals are excluded.
dirs := []point{{p.r - 1, p.c}, {p.r + 1, p.c}, {p.r, p.c - 1}, {p.r, p.c + 1}}
for _, d := range dirs {
// skip neighbors that would fall outside the grid before
// indexing into it, to avoid a panic at the edges/corners.
if d.r < 0 || d.r >= rows || d.c < 0 || d.c >= cols {
continue
}
// skip water, and skip land already claimed by this (or an
// earlier) BFS - both cases mean there's nothing new to add.
if visited[d.r][d.c] || grid[d.r][d.c] != '1' {
continue
}
visited[d.r][d.c] = true
queue = append(queue, d)
}
}
}
count := 0
// scanning every cell guarantees no island is missed regardless of its
// shape or position; the !visited check is what makes each island only
// ever trigger one bfs call - once flood-filled, its cells are skipped
// for the rest of the scan.
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] == '1' && !visited[r][c] {
count++
bfs(r, c)
}
}
}
return count
}
func printGrid(grid [][]byte) {
for _, row := range grid {
fmt.Println(string(row))
}
}
func main() {
grid1 := [][]byte{
[]byte("11110"),
[]byte("11010"),
[]byte("11000"),
[]byte("00000"),
}
fmt.Println("grid 1:")
printGrid(grid1)
fmt.Printf("islands=%d\n\n", numIslands(grid1))
grid2 := [][]byte{
[]byte("11000"),
[]byte("11000"),
[]byte("00100"),
[]byte("00011"),
}
fmt.Println("grid 2:")
printGrid(grid2)
fmt.Printf("islands=%d\n", numIslands(grid2))
}