Table of Contents
- Go Coding Challenges
- Table of Contents
- 1. 🟢 Reverse a String (Unicode-aware)
- 2. 🟢 FizzBuzz
- 3. 🟢 Two Sum (Map)
- 4. 🟢 Valid Anagram
- 5. 🟢 Binary Search
- 6. 🟡 Word Frequency Counter
- 7. 🟡 Maximum Subarray (Kadane)
- 8. 🟡 Merge Intervals
- 9. 🟡 Detect Cycle in a Linked List
- 10. 🟡 BFS and DFS on a Graph
- 11. 🟡 Concurrent-Safe Counter
- 12. 🔴 LRU Cache
- 13. 🔴 Token-Bucket Rate Limiter
- 14. 🔴 Worker Pool with errgroup
- 15. 🔴 Fan-Out / Fan-In Pipeline with Context Cancellation
Go Coding Challenges¶
Fifteen interview-style coding challenges ordered from beginner to advanced. Each one ships with a problem statement, a collapsible hint, an idiomatic Go solution, and a complexity note.
Difficulty legend: 🟢 beginner · 🟡 intermediate · 🔴 advanced
Table of Contents¶
- 🟢 Reverse a String (Unicode-aware)
- 🟢 FizzBuzz
- 🟢 Two Sum (Map)
- 🟢 Valid Anagram
- 🟢 Binary Search
- 🟡 Word Frequency Counter
- 🟡 Maximum Subarray (Kadane)
- 🟡 Merge Intervals
- 🟡 Detect Cycle in a Linked List
- 🟡 BFS and DFS on a Graph
- 🟡 Concurrent-Safe Counter
- 🔴 LRU Cache
- 🔴 Token-Bucket Rate Limiter
- 🔴 Worker Pool with errgroup
- 🔴 Fan-Out / Fan-In Pipeline with Context Cancellation
1. 🟢 Reverse a String (Unicode-aware)¶
Problem: Reverse a string so that multi-byte runes (emoji, accented characters, CJK) stay intact. A naive byte reversal corrupts UTF-8.
Hint
A Go `string` is a read-only slice of bytes. Iterating with `range` decodes runes for you, or convert with `[]rune(s)`. Reverse the rune slice in place with a two-pointer swap, then convert back to `string`.package main
import "fmt"
// ReverseString reverses s by code point, keeping multi-byte runes intact.
func ReverseString(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
func main() {
fmt.Println(ReverseString("Hello, 世界 🌍")) // "🌍 界世 ,olleH"
}
Complexity: Time O(n), Space O(n) for the rune slice.
2. 🟢 FizzBuzz¶
Problem: Print numbers 1..n. For multiples of 3 print "Fizz", for multiples of 5 print "Buzz", and for multiples of both print "FizzBuzz".
Hint
Build the string by appending. Check `i%3` and `i%5` independently so the "FizzBuzz" case falls out naturally, then fall back to the number when the accumulator is still empty.package main
import (
"fmt"
"strconv"
)
// FizzBuzz returns the FizzBuzz sequence for 1..n.
func FizzBuzz(n int) []string {
out := make([]string, 0, n)
for i := 1; i <= n; i++ {
s := ""
if i%3 == 0 {
s += "Fizz"
}
if i%5 == 0 {
s += "Buzz"
}
if s == "" {
s = strconv.Itoa(i)
}
out = append(out, s)
}
return out
}
func main() {
fmt.Println(FizzBuzz(15))
}
Complexity: Time O(n), Space O(n) for the output.
3. 🟢 Two Sum (Map)¶
Problem: Given an integer slice and a target, return the indices of the two numbers that add up to the target. Assume exactly one solution exists.
Hint
One pass. For each value `v` at index `i`, ask the map whether you have already seen `target-v`. Store `value -> index` as you go so you never reuse an element.package main
import "fmt"
// TwoSum returns the indices of the two values summing to target, or ok=false.
func TwoSum(nums []int, target int) (int, int, bool) {
seen := make(map[int]int, len(nums))
for i, v := range nums {
if j, ok := seen[target-v]; ok {
return j, i, true
}
seen[v] = i
}
return 0, 0, false
}
func main() {
i, j, ok := TwoSum([]int{2, 7, 11, 15}, 9)
fmt.Println(i, j, ok) // 0 1 true
}
Complexity: Time O(n), Space O(n) for the map.
4. 🟢 Valid Anagram¶
Problem: Determine whether two strings are anagrams of each other, counting Unicode code points (not bytes).
Hint
Anagrams have identical character multisets. Tally counts from the first string in a `map[rune]int`, then decrement while scanning the second. Any non-zero residual (or a length mismatch) means they are not anagrams.package main
import "fmt"
// IsAnagram reports whether a and b contain the same runes with equal counts.
func IsAnagram(a, b string) bool {
ra, rb := []rune(a), []rune(b)
if len(ra) != len(rb) {
return false
}
counts := make(map[rune]int, len(ra))
for _, r := range ra {
counts[r]++
}
for _, r := range rb {
counts[r]--
if counts[r] < 0 {
return false
}
}
return true
}
func main() {
fmt.Println(IsAnagram("listen", "silent")) // true
fmt.Println(IsAnagram("rat", "car")) // false
}
Complexity: Time O(n), Space O(k) for k distinct runes.
5. 🟢 Binary Search¶
Problem: Find the index of target in a sorted slice, or return -1 if it is
absent.
Hint
Maintain a half-open search window `[lo, hi)`. Compute `mid` as `lo + (hi-lo)/2` to avoid overflow. Shrink the window based on the comparison until it is empty.package main
import "fmt"
// BinarySearch returns the index of target in the sorted slice, or -1.
func BinarySearch(nums []int, target int) int {
lo, hi := 0, len(nums)
for lo < hi {
mid := lo + (hi-lo)/2
switch {
case nums[mid] == target:
return mid
case nums[mid] < target:
lo = mid + 1
default:
hi = mid
}
}
return -1
}
func main() {
fmt.Println(BinarySearch([]int{1, 3, 5, 7, 9}, 7)) // 3
fmt.Println(BinarySearch([]int{1, 3, 5, 7, 9}, 4)) // -1
}
Complexity: Time O(log n), Space O(1).
6. 🟡 Word Frequency Counter¶
Problem: Count word frequencies in text, case-insensitively, and return the
top k words ordered by descending count (ties broken alphabetically).
Hint
Tokenize with `strings.FieldsFunc`, lowercase, and tally into a map. Move the pairs into a slice and `sort.Slice` with a comparator that orders by count descending then word ascending. Slice off the top k.package main
import (
"fmt"
"sort"
"strings"
"unicode"
)
type wordCount struct {
Word string
Count int
}
// TopWords returns the k most frequent words in text, case-insensitively.
func TopWords(text string, k int) []wordCount {
counts := make(map[string]int)
fields := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
})
for _, w := range fields {
counts[w]++
}
pairs := make([]wordCount, 0, len(counts))
for w, c := range counts {
pairs = append(pairs, wordCount{w, c})
}
sort.Slice(pairs, func(i, j int) bool {
if pairs[i].Count != pairs[j].Count {
return pairs[i].Count > pairs[j].Count
}
return pairs[i].Word < pairs[j].Word
})
if k > len(pairs) {
k = len(pairs)
}
return pairs[:k]
}
func main() {
fmt.Println(TopWords("the cat the dog the bird a cat", 2))
// [{the 3} {cat 2}]
}
Complexity: Time O(n + m log m) for m distinct words, Space O(m).
7. 🟡 Maximum Subarray (Kadane)¶
Problem: Find the maximum sum of any contiguous subarray within an integer slice.
Hint
Track the best sum ending at the current index: extend the previous run or restart at the current element, whichever is larger. Keep a running global maximum. Handle the all-negative case by seeding from the first element.package main
import "fmt"
// MaxSubArray returns the largest sum of any contiguous subarray.
// Panics on an empty slice, by definition there is no subarray.
func MaxSubArray(nums []int) int {
if len(nums) == 0 {
panic("MaxSubArray: empty slice")
}
best, cur := nums[0], nums[0]
for _, v := range nums[1:] {
if cur+v > v {
cur += v
} else {
cur = v
}
if cur > best {
best = cur
}
}
return best
}
func main() {
fmt.Println(MaxSubArray([]int{-2, 1, -3, 4, -1, 2, 1, -5, 4})) // 6
}
Complexity: Time O(n), Space O(1).
8. 🟡 Merge Intervals¶
Problem: Given a list of [start, end] intervals, merge all overlapping
intervals and return the result sorted by start.
Hint
Sort by start. Walk the sorted list keeping the last merged interval; if the next start is within the current end, extend the end, otherwise push a new interval. Copy intervals before sorting if the caller's slice must stay intact.package main
import (
"fmt"
"sort"
)
// MergeIntervals merges overlapping [start, end] intervals.
func MergeIntervals(intervals [][2]int) [][2]int {
if len(intervals) == 0 {
return nil
}
sorted := make([][2]int, len(intervals))
copy(sorted, intervals)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i][0] < sorted[j][0]
})
merged := [][2]int{sorted[0]}
for _, iv := range sorted[1:] {
last := &merged[len(merged)-1]
if iv[0] <= last[1] {
if iv[1] > last[1] {
last[1] = iv[1]
}
} else {
merged = append(merged, iv)
}
}
return merged
}
func main() {
fmt.Println(MergeIntervals([][2]int{{1, 3}, {2, 6}, {8, 10}, {15, 18}}))
// [[1 6] [8 10] [15 18]]
}
Complexity: Time O(n log n), Space O(n).
9. 🟡 Detect Cycle in a Linked List¶
Problem: Determine whether a singly linked list contains a cycle.
Hint
Floyd's tortoise and hare: advance one pointer by one node and another by two. If they ever meet, there is a cycle; if the fast pointer reaches `nil`, the list is acyclic. Constant extra space.package main
import "fmt"
// ListNode is a singly linked list node.
type ListNode struct {
Val int
Next *ListNode
}
// HasCycle reports whether the list starting at head contains a cycle.
func HasCycle(head *ListNode) bool {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
func main() {
a := &ListNode{Val: 1}
b := &ListNode{Val: 2}
c := &ListNode{Val: 3}
a.Next, b.Next, c.Next = b, c, a // cycle back to a
fmt.Println(HasCycle(a)) // true
}
Complexity: Time O(n), Space O(1).
10. 🟡 BFS and DFS on a Graph¶
Problem: Given an adjacency-list graph, return the order of nodes visited from a start node using both breadth-first and depth-first traversal.
Hint
Use a `map[int][]int` for adjacency and a `visited` set. BFS uses a FIFO queue; DFS uses recursion (or an explicit stack). Mark nodes visited when you enqueue or first reach them to avoid revisiting cycles.package main
import "fmt"
type Graph map[int][]int
// BFS returns nodes in breadth-first order from start.
func (g Graph) BFS(start int) []int {
visited := map[int]bool{start: true}
queue := []int{start}
var order []int
for len(queue) > 0 {
n := queue[0]
queue = queue[1:]
order = append(order, n)
for _, nb := range g[n] {
if !visited[nb] {
visited[nb] = true
queue = append(queue, nb)
}
}
}
return order
}
// DFS returns nodes in depth-first order from start.
func (g Graph) DFS(start int) []int {
visited := make(map[int]bool)
var order []int
var walk func(int)
walk = func(n int) {
visited[n] = true
order = append(order, n)
for _, nb := range g[n] {
if !visited[nb] {
walk(nb)
}
}
}
walk(start)
return order
}
func main() {
g := Graph{0: {1, 2}, 1: {2}, 2: {0, 3}, 3: {3}}
fmt.Println(g.BFS(2)) // [2 0 3 1]
fmt.Println(g.DFS(2)) // [2 0 1 3]
}
Complexity: Time O(V + E), Space O(V).
11. 🟡 Concurrent-Safe Counter¶
Problem: Implement a counter that many goroutines can increment safely.
Show both a sync.Mutex version and a lock-free sync/atomic version.
Hint
A bare `count++` is a read-modify-write and races under concurrency. Guard it with a `sync.Mutex` (defer the unlock) or use `atomic.Int64`, which serializes the operation in hardware. Use a `WaitGroup` so the caller waits for all writers.package main
import (
"fmt"
"sync"
"sync/atomic"
)
// MutexCounter is safe for concurrent use via a mutex.
type MutexCounter struct {
mu sync.Mutex
count int64
}
func (c *MutexCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *MutexCounter) Value() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// AtomicCounter is a lock-free alternative.
type AtomicCounter struct {
count atomic.Int64
}
func (c *AtomicCounter) Inc() { c.count.Add(1) }
func (c *AtomicCounter) Value() int64 { return c.count.Load() }
func main() {
const goroutines, perG = 100, 1000
c := &AtomicCounter{}
var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < perG; j++ {
c.Inc()
}
}()
}
wg.Wait()
fmt.Println(c.Value()) // 100000
}
Complexity: Time O(n) increments, Space O(1). Run with go run -race to verify.
12. 🔴 LRU Cache¶
Problem: Implement a fixed-capacity Least-Recently-Used cache with O(1)
Get and Put. Accessing or updating a key marks it most-recently-used; on
overflow, evict the least-recently-used entry.
Hint
Combine a `map[key]*list.Element` for O(1) lookup with a `container/list` doubly linked list for O(1) recency reordering. Store the key inside the list value so eviction can delete the map entry. Move touched nodes to the front; evict from the back.package main
import (
"container/list"
"fmt"
)
type entry struct {
key, value int
}
// LRUCache is a fixed-capacity LRU cache with O(1) operations.
type LRUCache struct {
cap int
ll *list.List // front = most recently used
items map[int]*list.Element // key -> element holding *entry
}
func NewLRUCache(capacity int) *LRUCache {
return &LRUCache{
cap: capacity,
ll: list.New(),
items: make(map[int]*list.Element, capacity),
}
}
func (c *LRUCache) Get(key int) (int, bool) {
if el, ok := c.items[key]; ok {
c.ll.MoveToFront(el)
return el.Value.(*entry).value, true
}
return 0, false
}
func (c *LRUCache) Put(key, value int) {
if el, ok := c.items[key]; ok {
el.Value.(*entry).value = value
c.ll.MoveToFront(el)
return
}
if c.ll.Len() >= c.cap {
oldest := c.ll.Back()
if oldest != nil {
c.ll.Remove(oldest)
delete(c.items, oldest.Value.(*entry).key)
}
}
c.items[key] = c.ll.PushFront(&entry{key, value})
}
func main() {
c := NewLRUCache(2)
c.Put(1, 1)
c.Put(2, 2)
fmt.Println(c.Get(1)) // 1 true
c.Put(3, 3) // evicts key 2
fmt.Println(c.Get(2)) // 0 false
}
Complexity: Time O(1) per op, Space O(capacity).
13. 🔴 Token-Bucket Rate Limiter¶
Problem: Build a thread-safe token-bucket limiter. Tokens refill at a fixed
rate up to a burst capacity; Allow consumes one token and reports whether the
call is permitted.
Hint
Track tokens as a float and the timestamp of the last refill. On each `Allow`, lazily add `elapsed * rate` tokens (capped at burst), then consume one if available. Guard shared state with a mutex. Lazy refill avoids a background ticker goroutine.package main
import (
"fmt"
"sync"
"time"
)
// TokenBucket is a thread-safe token-bucket rate limiter.
type TokenBucket struct {
mu sync.Mutex
tokens float64
burst float64
ratePerS float64
last time.Time
}
// NewTokenBucket creates a bucket that refills ratePerSecond tokens per second,
// capped at burst, and starts full.
func NewTokenBucket(ratePerSecond, burst float64) *TokenBucket {
return &TokenBucket{
tokens: burst,
burst: burst,
ratePerS: ratePerSecond,
last: time.Now(),
}
}
// Allow consumes a token if one is available.
func (b *TokenBucket) Allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
elapsed := now.Sub(b.last).Seconds()
b.last = now
b.tokens += elapsed * b.ratePerS
if b.tokens > b.burst {
b.tokens = b.burst
}
if b.tokens >= 1 {
b.tokens--
return true
}
return false
}
func main() {
b := NewTokenBucket(5, 2) // 5/s, burst 2
fmt.Println(b.Allow()) // true
fmt.Println(b.Allow()) // true
fmt.Println(b.Allow()) // false (burst exhausted)
}
Complexity: Time O(1) per call, Space O(1). (Production code can use golang.org/x/time/rate.)
14. 🔴 Worker Pool with errgroup¶
Problem: Process a stream of jobs across a bounded pool of workers. Collect results, stop early on the first error, and cancel outstanding work without leaking goroutines.
Hint
`golang.org/x/sync/errgroup` with `errgroup.WithContext` gives you a derived context that is cancelled on the first error. Fan jobs out over a channel, spawn N workers via `g.Go`, and select on `ctx.Done()` inside both producer and workers so cancellation propagates. `g.Wait()` returns the first error.package main
import (
"context"
"fmt"
"sort"
"sync"
"golang.org/x/sync/errgroup"
)
// SquareAll squares each input concurrently using a bounded worker pool.
func SquareAll(ctx context.Context, inputs []int, workers int) ([]int, error) {
g, ctx := errgroup.WithContext(ctx)
jobs := make(chan int)
// Producer: feed jobs, stop if the context is cancelled.
g.Go(func() error {
defer close(jobs)
for _, n := range inputs {
select {
case jobs <- n:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
var (
mu sync.Mutex
results []int
)
for i := 0; i < workers; i++ {
g.Go(func() error {
for n := range jobs {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
mu.Lock()
results = append(results, n*n)
mu.Unlock()
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
sort.Ints(results)
return results, nil
}
func main() {
res, err := SquareAll(context.Background(), []int{1, 2, 3, 4, 5}, 3)
fmt.Println(res, err) // [1 4 9 16 25] <nil>
}
Complexity: Time O(n) work over W workers, Space O(n). Requires golang.org/x/sync.
15. 🔴 Fan-Out / Fan-In Pipeline with Context Cancellation¶
Problem: Build a three-stage pipeline (generate -> fan-out transform -> fan-in) using channels. It must shut down cleanly when the caller cancels the context, with no leaked goroutines.
Hint
Each stage returns a receive-only channel and owns closing it. Use a `select` on `ctx.Done()` at every send so a cancellation unblocks producers. For fan-in, spawn one forwarding goroutine per source and close the merged channel once a `sync.WaitGroup` reports all sources are drained.package main
import (
"context"
"fmt"
"sync"
)
// gen emits the inputs on a channel, stopping on cancellation.
func gen(ctx context.Context, nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
// square transforms one input channel (a fan-out worker).
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
// merge fans multiple channels into one (fan-in).
func merge(ctx context.Context, chans ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chans))
for _, c := range chans {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
select {
case out <- v:
case <-ctx.Done():
return
}
}
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
src := gen(ctx, 1, 2, 3, 4, 5)
// Fan-out across two workers, then fan-in.
out := merge(ctx, square(ctx, src), square(ctx, src))
sum := 0
for v := range out {
sum += v
}
fmt.Println(sum) // 55
}
Complexity: Time O(n) over the pipeline, Space O(1) per stage buffer. Run with -race to confirm no leaks.