Prev Next

Golang / GoLang Concurrency Mastery Interview Questions

1. Explain Go's GMP scheduler model. What are M, P, and G and how do they interact? 2. What is work stealing in Go's scheduler and why does it matter for performance? 3. Why can Go run millions of goroutines while equivalent OS-thread workloads fail? 4. What is the difference between unbuffered and buffered channels in Go? 5. What happens when you send to, receive from, or close a nil or closed channel? 6. How does the select statement work in Go and what are its key properties? 7. What is a data race in Go, how do you detect it, and what are the three main fixes? 8. When do you use sync.Mutex versus sync.RWMutex, and what are the critical usage rules? 9. How does sync.WaitGroup work and what are the most common mistakes? 10. What is sync.Once and what guarantees does it provide? 11. What causes deadlocks in Go and how do you detect and prevent them? 12. What are directional channels in Go and why use them in function signatures? 13. What causes goroutine leaks and how do you prevent and detect them? 14. Implement fan-out and fan-in concurrency patterns in Go. 15. How do you use a nil channel in select to dynamically enable or disable cases? 16. When should you use the sync/atomic package instead of sync.Mutex? 17. Implement a bounded worker pool pattern in Go. 18. What are the differences between time.After, time.NewTimer, and time.NewTicker, and which leaks resources? 19. How does context.Context enable clean goroutine cancellation and why is 'defer cancel()' critical? 20. How do you use a buffered channel as a semaphore to limit goroutine concurrency? 21. What is the 'done channel' pattern and why has context.Context largely superseded it? 22. What is the goroutine loop-variable capture bug and how do you fix it? 23. How does golang.org/x/sync/errgroup simplify concurrent error handling? 24. When should a channel carry 'chan struct{}' versus a typed value, and why is close() used for broadcast? 25. What is GOMAXPROCS, how does it affect parallelism, and what is the container pitfall? 26. How does Go's select handle multiple ready cases, and how do you implement true priority? 27. What is sync.Cond and when do you use it instead of channels? 28. Implement Go's canonical pipeline pattern with cancellation from the Go blog. 29. What is Go's memory model and why does it matter for concurrent code? 30. What is the check-then-act race condition (TOCTOU) and how do you fix it? 31. What is asynchronous preemption in Go (1.14+) and why was it introduced? 32. How do you safely use a map from multiple goroutines in Go? 33. How do you use a buffered channel as a task queue with natural backpressure? 34. How does Go handle goroutines that make blocking syscalls — what happens to M and P? 35. Implement a simple publish-subscribe broker using Go channels. 36. What is the lock-held-during-I/O anti-pattern and how do you fix it? 37. Write a complete example of implementing operation timeouts in Go using select. 38. How do you write tests that detect goroutine leaks automatically? 39. Implement a concurrent word count across multiple files — a classic Go interview puzzle. 40. How does GOMAXPROCS=1 change behaviour and when is it actually useful? 41. How do you implement a high-performance sharded concurrent map in Go? 42. What are the specific happens-before guarantees for channel operations in Go's memory model? 43. How do you implement a hedged request pattern using select and goroutines? 44. How does sync.Pool reduce GC pressure in high-throughput Go services? 45. What is a livelock and how does it differ from a deadlock in Go programs? 46. How do you implement backpressure in Go to prevent overloading downstream systems? 47. Implement a lock-free stack using atomic CAS operations and explain the ABA problem. 48. Summarise: channel vs mutex decision guide, and the top concurrency pitfalls.
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. Explain Go's GMP scheduler model. What are M, P, and G and how do they interact?

Go uses an M:N scheduler — M goroutines multiplexed onto N OS threads, managed by the Go runtime. The three key entities are:

GMP Entities
EntitySymbolDescription
GoroutineGLightweight user-space thread with its own 2 KB stack. Contains the goroutine's code, stack pointer, and scheduling state.
Machine (OS thread)MA real kernel thread that executes Go code. The number of active Ms equals GOMAXPROCS by default.
Processor (logical CPU)PA scheduling context holding a local run queue of runnable Gs. One P per logical CPU (controlled by GOMAXPROCS). An M must hold a P to execute Go code.
import "runtime"

// GOMAXPROCS sets the number of Ps (default = runtime.NumCPU())
runtime.GOMAXPROCS(4) // 4 Ps → up to 4 goroutines running in true parallel

// Query without changing
numP := runtime.GOMAXPROCS(0)
fmt.Println("Ps:", numP)

// Each P holds a local run queue: a lock-free ring buffer of ~256 G slots
// There is also a global run queue for overflow

// Work stealing: when a P's local queue is empty,
// it steals half the Gs from another P's queue
// This keeps all CPUs busy without a central scheduling lock

Goroutine lifecycle through GMP: (1) go func() creates a G and places it in the current P's local run queue. (2) The M bound to that P picks up the G and executes it. (3) If the G makes a blocking syscall, the M detaches from its P; the P binds to a new M and continues running other Gs. When the syscall returns, the original M tries to reacquire a P — if none is free, the G goes to the global queue and M parks. (4) Asynchronous preemption (Go 1.14+): a goroutine running too long without a function call receives a SIGURG signal and is preempted.

What does a P (Processor) in Go's GMP model hold?
What happens to the P when a goroutine makes a blocking syscall?
2. What is work stealing in Go's scheduler and why does it matter for performance?

Work stealing is the mechanism that keeps all Ps (logical CPUs) busy even when goroutine load is unevenly distributed. It is the key reason Go programs efficiently use all available CPU cores without manual thread pool management.

How it works: each P maintains a local run queue — a lock-free ring buffer of up to 256 runnable Gs. When a P's local queue is empty, instead of blocking, it steals approximately half the Gs from another P's local queue. It also checks the global run queue and the network poller.

// Scheduling check order when a P's local queue is empty:
// 1. Every ~61 ticks: check the GLOBAL run queue first
//    (prevents global queue goroutines from starving)
// 2. Local run queue (lock-free)
// 3. Work-steal from another random P (takes ~half its Gs)
// 4. Global run queue
// 5. Network poller (goroutines blocked on net I/O now ready)

// Demonstration: 4 CPUs, 1000 goroutines — load distributes automatically
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        // This goroutine may be stolen from one P to another
        time.Sleep(10 * time.Millisecond)
    }(i)
}
wg.Wait()

// Visualise stealing with execution tracer:
// f, _ := os.Create("trace.out")
// trace.Start(f); ...; trace.Stop()
// go tool trace trace.out  →  shows G→P migrations

Why the 61-tick global check? Without it, goroutines that land in the global queue (due to overflow or waking from syscalls) could starve while all Ps have busy local queues. The periodic check ensures fairness.

What does a P do when its local run queue is empty?
Why does Go check the global run queue every ~61 scheduling ticks rather than always checking local queues first?
3. Why can Go run millions of goroutines while equivalent OS-thread workloads fail?

Three fundamental differences make goroutines dramatically cheaper than OS threads: initial stack size, scheduling cost, and blocking behaviour.

Goroutine vs OS Thread
AspectOS ThreadGoroutine
Initial stack1–8 MB (fixed, kernel-allocated)2 KB (grows dynamically up to 1 GB)
SchedulingPreemptive by OS (expensive context switch with full register save)Cooperative + async-preemptive by Go runtime (user-space, cheap)
BlockingBlocks the entire OS thread on syscallParks goroutine; OS thread freed for other goroutines
Creation cost~10 µs, requires kernel call~300 ns, entirely user-space
Practical limit~10,000 before memory exhaustion~1,000,000+ on standard hardware
package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    const n = 1_000_000
    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            // Each goroutine: ~2 KB stack at creation
            // 1M goroutines ≈ 2 GB total — feasible on modern hardware
        }()
    }
    wg.Wait()
    fmt.Println("done, goroutines:", runtime.NumGoroutine())
}

The small initial stack is possible because Go uses copy-on-grow stacks: when a goroutine needs more stack space the runtime allocates a new, larger stack, copies the old contents, updates all internal pointers, and frees the old stack. This is transparent to user code.

What is the default initial stack size for a newly spawned goroutine in Go?
Which function returns the current number of live goroutines in a running Go program?
4. What is the difference between unbuffered and buffered channels in Go?

This is the most fundamental channel question in every Go interview. The two types have completely different synchronisation semantics.

Unbuffered vs Buffered Channel
AspectUnbuffered (make(chan T))Buffered (make(chan T, N))
Capacity0N
Send blocks whenNo goroutine is ready to receiveBuffer is already full (N items queued)
Receive blocks whenNo goroutine is ready to sendBuffer is empty
Synchronisation modelSynchronous rendezvous — sender and receiver meet at the channelAsynchronous up to capacity
Typical use caseSignalling, handshake, guaranteed delivery confirmationDecoupling producer/consumer throughput rates
// Unbuffered — sender parks until a receiver is ready
ch := make(chan int)
go func() { ch <- 42 }()  // goroutine parks until main receives
v := <-ch                  // unblocks the sender
fmt.Println(v)             // 42

// Buffered — send does not block until buffer is full
bch := make(chan int, 3)
bch <- 1   // no goroutine needed — value goes into buffer
bch <- 2
bch <- 3
// bch <- 4  // BLOCKS — buffer full, no receiver

fmt.Println(<-bch) // 1  (FIFO ordering)
fmt.Println(<-bch) // 2
fmt.Println(<-bch) // 3

// Inspect buffer state
fmt.Println(len(bch), cap(bch)) // 0  3

// Classic deadlock with unbuffered channel in same goroutine
// ch2 := make(chan int)
// ch2 <- 99  // DEADLOCK: send blocks, nobody ever receives

Zero-copy optimisation for unbuffered channels: when a sender and receiver goroutine meet at an unbuffered channel, Go copies the data directly from the sender's stack to the receiver's — no intermediate heap allocation. This is why unbuffered channels have the lowest latency of any inter-goroutine communication mechanism.

Which statement about unbuffered channels in Go is correct?
What does len(ch) return for a buffered channel make(chan int, 5) that currently holds 3 items?
5. What happens when you send to, receive from, or close a nil or closed channel?

This table is essential interview knowledge. Mistakes here produce panics and deadlocks that are notoriously difficult to debug.

Channel Operation Reference Table
OperationNil channelOpen, empty channelOpen, has dataClosed channel
Send (ch <- v)Blocks foreverBlocksSends OKPANIC
Receive (<-ch)Blocks foreverBlocksReturns value, ok=trueReturns zero value, ok=false
Close (close(ch))PANICCloses successfullyCloses; remaining data still readablePANIC
// Nil channel: always blocks (send or receive)
var ch chan int
// ch <- 1    // deadlock
// <-ch       // deadlock
// close(ch)  // PANIC: close of nil channel

// Closed channel: reads drain buffered data then return zero
done := make(chan struct{})
close(done)
v, ok := <-done
fmt.Println(v, ok)      // {} false
// done <- struct{}{} // PANIC: send on closed channel

// Comma-ok idiom: detect channel closure
ch2 := make(chan int, 2)
ch2 <- 1; ch2 <- 2
close(ch2)
for {
    v, ok := <-ch2
    if !ok { break }   // channel closed and drained
    fmt.Println(v)
}

// Idiomatic: range handles closure automatically
ch3 := make(chan int)
go func() { ch3 <- 1; ch3 <- 2; close(ch3) }()
for v := range ch3 { fmt.Println(v) } // exits on close

// GOLDEN RULE: only the SENDER should close a channel
// A receiver cannot know when the sender is done
What does receiving from a closed, empty buffered channel return?
Who should call close() on a channel, and why?
6. How does the select statement work in Go and what are its key properties?

select is Go's multiplexed channel operation. It waits on multiple channel operations simultaneously and proceeds with the first one that is ready. It is the primary tool for non-blocking operations, timeouts, and cancellation in concurrent code.

// Basic select — whichever channel has data first
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "one"
ch2 <- "two"
select {
case msg := <-ch1:
    fmt.Println("ch1:", msg)
case msg := <-ch2:
    fmt.Println("ch2:", msg)
}
// If MULTIPLE cases are ready: Go picks ONE AT RANDOM

// Non-blocking operation with default
ch := make(chan int, 1)
select {
case v := <-ch:
    fmt.Println("received:", v)
default:
    fmt.Println("no value ready — returns immediately")
}

// Timeout pattern
func fetchWithTimeout(url string, d time.Duration) ([]byte, error) {
    result := make(chan []byte, 1) // buffered — prevents goroutine leak!
    go func() {
        resp, _ := http.Get(url)
        body, _ := io.ReadAll(resp.Body)
        result <- body
    }()
    select {
    case data := <-result:
        return data, nil
    case <-time.After(d):
        return nil, fmt.Errorf("timeout after %v", d)
    }
}

// Cancellation with context (production preferred)
func processJobs(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            return
        case job, ok := <-jobs:
            if !ok { return }
            process(job)
        }
    }
}

Nil channel trick: a case on a nil channel is permanently disabled — it never fires. This allows you to dynamically enable and disable select cases by toggling a channel variable between nil and a real channel.

If multiple cases in a select statement are simultaneously ready, which one executes?
What does a 'default' case in a select statement do?
7. What is a data race in Go, how do you detect it, and what are the three main fixes?

A data race occurs when two or more goroutines access the same memory location concurrently, at least one access is a write, and there is no synchronisation between them. Data races produce undefined behaviour: silent data corruption, non-deterministic results, or rare crashes.

// Classic data race — unsynchronised increment from multiple goroutines
var counter int
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++  // DATA RACE: read-increment-write is not atomic
    }()
}
wg.Wait()
fmt.Println(counter) // may print anything less than 1000

// DETECT: go run -race main.go   OR   go test -race ./...
// Race detector output:
// ==================
// WARNING: DATA RACE
// Write at 0x... by goroutine 7:  main.main.func1() :11
// Previous write at 0x... by goroutine 6: main.main.func1() :11
// ==================

// FIX 1: sync.Mutex
var mu sync.Mutex
var safe int
go func() { mu.Lock(); safe++; mu.Unlock() }()

// FIX 2: atomic (faster for simple numeric ops)
var atomic64 int64
go func() { atomic.AddInt64(&atomic64, 1) }()

// FIX 3: channel — single goroutine owns the variable
type incMsg struct{}
ch := make(chan incMsg)
go func() {
    n := 0
    for range ch { n++ }
}()

The race detector adds 5–10× CPU overhead and uses ThreadSanitizer (TSan) under the hood. It reports every detected race with full stack traces for both the current and prior conflicting access — making the root cause straightforward to identify. Always run your test suite with -race.

Which command enables the Go race detector when running tests?
Why is 'counter++' a data race when executed from multiple goroutines without synchronisation?

8. When do you use sync.Mutex versus sync.RWMutex, and what are the critical usage rules?

sync.Mutex provides mutual exclusion — at most one goroutine holds the lock at any time. sync.RWMutex distinguishes readers from writers, allowing multiple concurrent readers OR one exclusive writer — more efficient for read-heavy workloads.

// sync.Mutex — mixed or write-heavy access
type BankAccount struct {
    mu      sync.Mutex
    balance float64
}

func (a *BankAccount) Deposit(amount float64) {
    a.mu.Lock()
    defer a.mu.Unlock() // ALWAYS defer — fires even on panic or early return
    a.balance += amount
}

func (a *BankAccount) Balance() float64 {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.balance
}

// sync.RWMutex — read-heavy (reads >> writes)
type SafeConfig struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeConfig) Get(key string) string {
    c.mu.RLock()          // multiple goroutines can RLock at the same time
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *SafeConfig) Set(key, val string) {
    c.mu.Lock()           // exclusive: no readers OR writers allowed
    defer c.mu.Unlock()
    c.data[key] = val
}

// CRITICAL MISTAKES:
// 1. NEVER copy a Mutex after first use
//    Copying duplicates internal state → broken synchronisation
//    Always use *BankAccount, never BankAccount by value

// 2. Mutex is NOT reentrant
//    A goroutine that calls Lock() while already holding it → DEADLOCK

// 3. Writer starvation protection in RWMutex:
//    When a writer is waiting, new readers are blocked
//    even if existing readers still hold RLock
Mutex Decision Guide
ConditionPrefer
Write-heavy or mixed reads/writessync.Mutex
Read-dominant (e.g. config, cache, registry)sync.RWMutex
Very short critical section (<1 µs)sync.Mutex (RWMutex overhead can dominate)
Single integer countersync/atomic (no lock at all)
What happens if a goroutine that already holds a sync.Mutex calls Lock() again on the same mutex?
Why must you never copy a sync.Mutex by value after first use?
9. How does sync.WaitGroup work and what are the most common mistakes?

sync.WaitGroup is a counter-based synchronisation primitive. One goroutine calls Wait() to block until all tracked goroutines have called Done(). The counter starts at zero, increases with Add(n), and decreases with Done() (equivalent to Add(-1)).

// CORRECT usage pattern
var wg sync.WaitGroup

urls := []string{"http://a.com", "http://b.com", "http://c.com"}
for _, url := range urls {
    wg.Add(1)               // Add BEFORE launching — not inside the goroutine
    go func(u string) {
        defer wg.Done()     // defer ensures Done fires even on panic
        fetchURL(u)
    }(url)                  // Pass url as argument (avoids closure trap)
}
wg.Wait()                   // blocks until counter reaches zero

// MISTAKE 1: Add inside the goroutine → race with Wait
// go func() {
//     wg.Add(1)   // may execute AFTER wg.Wait() — already zero!
//     defer wg.Done()
// }()
// wg.Wait()  // may return before any goroutine calls Add

// MISTAKE 2: Passing WaitGroup by value
// processFiles(wg)   // WRONG: copies WaitGroup — broken
// processFiles(&wg)  // CORRECT: pass pointer

// MISTAKE 3: Calling Done() more times than Add() → panic
// wg.Done() // if counter is already zero, panics

// PATTERN: Add all at once when count is known
wg.Add(len(urls))
for _, url := range urls {
    go func(u string) {
        defer wg.Done()
        fetchURL(u)
    }(url)
}
wg.Wait()
Why must wg.Add(1) be called before launching the goroutine, not inside it?
What happens when wg.Done() is called more times than wg.Add() has been invoked?
10. What is sync.Once and what guarantees does it provide?

sync.Once guarantees that a function is executed exactly once, regardless of how many goroutines concurrently call Do(). It is the idiomatic Go approach for thread-safe lazy initialisation and singletons.

// Thread-safe singleton with sync.Once
var (
    instance *Database
    once     sync.Once
)

func GetDB() *Database {
    once.Do(func() {
        // Executes exactly once — all other goroutines block until complete
        instance = &Database{pool: openConnectionPool(config)}
    })
    return instance // always non-nil after once.Do returns
}

// GUARANTEE: even 1000 concurrent calls to GetDB()
// result in exactly one DB initialisation

// SUBTLE TRAP: if the function passed to Do panics,
// sync.Once considers it 'done' — future calls are no-ops
var o sync.Once
o.Do(func() { panic("init failed") }) // panics
o.Do(func() { /* NEVER RUNS */ })      // silently skipped

// Error-aware pattern
type result struct{ db *Database; err error }
var res result
var initOnce sync.Once

func getDB() (*Database, error) {
    initOnce.Do(func() { res.db, res.err = openDB() })
    return res.db, res.err
}

// Go 1.21+: sync.OnceValue / sync.OnceValues (ergonomic wrappers)
getConfig := sync.OnceValue(func() *Config { return loadConfig() })
cfg := getConfig() // computed once, cached forever
What happens when the function passed to sync.Once.Do() panics?
How many times does sync.Once.Do() execute the provided function, regardless of the number of concurrent callers?
11. What causes deadlocks in Go and how do you detect and prevent them?

A deadlock occurs when every goroutine is blocked waiting for a resource held by another — a circular dependency from which no goroutine can proceed. The Go runtime detects simple all-goroutine deadlocks and panics with 'all goroutines are asleep — deadlock!'.

// DEADLOCK 1: circular channel wait
ch1 := make(chan int)
ch2 := make(chan int)
go func() { v := <-ch1; ch2 <- v }() // waits for ch1, then sends ch2
go func() { v := <-ch2; ch1 <- v }() // waits for ch2, then sends ch1
// ALL goroutines blocked → runtime: 'all goroutines are asleep'

// DEADLOCK 2: non-reentrant mutex locked twice
var mu sync.Mutex
mu.Lock()
mu.Lock()   // DEADLOCK — tries to acquire a lock already held

// DEADLOCK 3: inconsistent lock-acquisition order
var muA, muB sync.Mutex
// goroutine 1: muA.Lock() then muB.Lock()  (A→B order)
// goroutine 2: muB.Lock() then muA.Lock()  (B→A order) → DEADLOCK

// FIX for #3: enforce a global consistent ordering — always A before B

// DEADLOCK 4: unbuffered send with no receiver
// ch := make(chan int)
// ch <- 1  // blocks forever

// Detection tools:
// 1. Go runtime: 'all goroutines are asleep'
// 2. CTRL+\  sends SIGQUIT → dumps all goroutine stacks
// 3. pprof goroutine endpoint: /debug/pprof/goroutine?debug=2
// 4. context.WithTimeout prevents indefinite blocking

Prevention rules: (1) acquire locks in a globally consistent order. (2) Prefer context.Context with deadlines over raw channel waits. (3) Use select with default or a timeout to avoid indefinite blocking. (4) Keep critical sections short and never hold a lock while performing I/O.

How does Go's runtime detect a simple deadlock?
What is the most reliable prevention strategy against multi-mutex deadlocks?
12. What are directional channels in Go and why use them in function signatures?

Go channels can be typed with a direction: chan<- T (send-only) or <-chan T (receive-only). A bidirectional chan T can be assigned to either. Directional channels enforce access discipline at compile time, making the data-flow contract of each function explicit.

// Producer: only sends to out — compile error if it tries to receive
func produce(out chan<- int) {
    for i := 0; i < 5; i++ { out <- i }
    close(out)   // close is allowed on a send-only channel
    // v := <-out  // COMPILE ERROR: receive from send-only channel
}

// Consumer: only receives from in
func consume(in <-chan int) {
    for v := range in { fmt.Println(v) }
    // in <- 99   // COMPILE ERROR: send to receive-only channel
    // close(in)  // COMPILE ERROR: close of receive-only channel
}

// Bidirectional channel can be passed as either direction
ch := make(chan int, 10) // chan int (bidirectional)
go produce(ch)          // narrowed to chan<- int automatically
consume(ch)             // narrowed to <-chan int automatically

// Pipeline: each stage returns receive-only — caller can't accidentally close
func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for _, n := range nums { out <- n } }()
    return out // exposes only read access
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for v := range in { out <- v * v } }()
    return out
}

for v := range square(generator(2, 3, 4)) {
    fmt.Println(v) // 4 9 16
}
What does 'chan<- T' mean in a Go function parameter?
Can a bidirectional 'chan int' be assigned to a send-only 'chan<- int' variable?
13. What causes goroutine leaks and how do you prevent and detect them?

A goroutine leak occurs when a goroutine starts but never terminates — it blocks indefinitely waiting on a channel, lock, or I/O that will never complete. Leaked goroutines accumulate over time, consuming memory and potentially holding references that prevent GC, causing steady memory growth until OOM.

// LEAK 1: nobody ever sends to ch — goroutine parks forever
func leak1() {
    ch := make(chan int)
    go func() {
        v := <-ch  // blocks; function returns; nobody sends → leaked
        process(v)
    }()
}

// FIX: use context for cancellation
func fixed1(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case v := <-ch:   process(v)
        case <-ctx.Done(): return
        }
    }()
}

// LEAK 2: jobs channel never closed
func leak2() {
    jobs := make(chan Job)
    go func() {
        for job := range jobs { process(job) } // waits forever
    }()
    // forgot close(jobs) → goroutine never exits
}

// LEAK 3: time.Ticker never stopped
func leak3() {
    ticker := time.NewTicker(time.Second)
    go func() {
        for range ticker.C { doWork() } // runs forever
    }()
    // forgot ticker.Stop() → goroutine and channel leaked
}

// DETECTION:
// runtime.NumGoroutine()         — watch for steady growth
// /debug/pprof/goroutine?debug=2 — full goroutine stack traces
// goleak library in tests:
// import "go.uber.org/goleak"
// defer goleak.VerifyNone(t)  // fails test if goroutines leak
What is the most reliable mechanism for ensuring a worker goroutine exits cleanly?
Which library provides automatic goroutine leak detection in Go unit tests?
14. Implement fan-out and fan-in concurrency patterns in Go.

Fan-out: distribute work from one source to multiple worker goroutines. Fan-in: merge results from multiple goroutines back into a single channel. Together they form Go's fundamental parallel pipeline pattern.

// Fan-out: distribute jobs to N workers
func fanOut(ctx context.Context, jobs <-chan Job, workers int) []<-chan Result {
    outputs := make([]<-chan Result, workers)
    for i := 0; i < workers; i++ {
        out := make(chan Result)
        outputs[i] = out
        go func(o chan<- Result) {
            defer close(o)
            for {
                select {
                case <-ctx.Done(): return
                case job, ok := <-jobs:
                    if !ok { return }
                    o <- process(job)
                }
            }
        }(out)
    }
    return outputs
}

// Fan-in: merge N result channels into one
func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result {
    var wg sync.WaitGroup
    merged := make(chan Result)

    forward := func(c <-chan Result) {
        defer wg.Done()
        for r := range c {
            select {
            case merged <- r:
            case <-ctx.Done(): return
            }
        }
    }

    wg.Add(len(channels))
    for _, c := range channels { go forward(c) }

    go func() { wg.Wait(); close(merged) }()
    return merged
}

// Wire it together
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
jobs := make(chan Job, 100)
go generateJobs(jobs)
results := fanIn(ctx, fanOut(ctx, jobs, 8)...)
for r := range results { fmt.Println(r) }
In the fan-in pattern, why must close(merged) be called inside a separate goroutine that waits on the WaitGroup?
What role does the context play in both fanOut and fanIn?
15. How do you use a nil channel in select to dynamically enable or disable cases?

A nil channel case in select is permanently disabled — it never fires. This allows you to dynamically enable or disable select cases at runtime by toggling a channel variable between nil and a real channel, without any if-else branching inside the select.

// Scenario: drain primary channel; only then activate secondary
func processOrdered(primary, secondary <-chan string) {
    activePrimary   := primary
    activeSecondary := (<-chan string)(nil) // disabled initially

    for activePrimary != nil || activeSecondary != nil {
        select {
        case v, ok := <-activePrimary:
            if !ok {
                activePrimary   = nil      // disable primary case
                activeSecondary = secondary // enable secondary case
            } else {
                fmt.Println("primary:", v)
            }
        case v, ok := <-activeSecondary:
            if !ok {
                activeSecondary = nil
            } else {
                fmt.Println("secondary:", v)
            }
        }
    }
}

// Another use: conditional send — nil channel means 'skip this send'
func maybeSend(notify chan<- string, msg string, doSend bool) {
    var ch chan<- string
    if doSend { ch = notify } // ch is nil if !doSend
    select {
    case ch <- msg: // disabled when ch is nil — never fires
    default:
    }
}

// Key property:
// A nil channel in select is excluded from consideration entirely.
// It does not block; it does not fire. It simply doesn't exist.
What happens when a select statement contains a case reading from a nil channel?
In the 'drain primary then activate secondary' example, why is setting activePrimary = nil the correct approach?
16. When should you use the sync/atomic package instead of sync.Mutex?

The sync/atomic package provides lock-free operations on individual primitive values using CPU-level instructions (LOCK prefix on x86, load-linked/store-conditional on ARM). It is faster than a mutex for simple single-variable operations but is limited to supported types and single-variable updates.

import "sync/atomic"

// AtomicInt64 — no lock needed
var counter int64
atomic.AddInt64(&counter, 1)           // atomic increment
v := atomic.LoadInt64(&counter)         // atomic read
atomic.StoreInt64(&counter, 0)          // atomic write

// Compare-and-Swap (CAS) — the foundation of lock-free algorithms
swapped := atomic.CompareAndSwapInt64(&counter, 0, 100)
// Sets counter=100 ONLY if counter==0; returns true if swap happened
fmt.Println(swapped, atomic.LoadInt64(&counter)) // true 100

// atomic.Value — atomically store/load any interface value
var config atomic.Value
config.Store(&Config{Timeout: 30 * time.Second}) // must always store same concrete type
cfg := config.Load().(*Config)

// Performance comparison (approximate, uncontended):
// atomic.AddInt64:      ~5  ns
// sync.Mutex Lock+Unlock: ~25 ns (uncontended)
// sync.Mutex Lock+Unlock: ~200 ns (contended)

// Use atomic when:
// ✓ Simple counters, flags, sequence numbers
// ✓ Publishing a single pointer or value others read
// ✓ High-frequency metrics (LongAdder / sharded counter pattern)

// Use Mutex when:
// ✓ Multiple related variables must be updated together atomically
// ✓ Non-trivial read-modify-write patterns
// ✓ Protecting complex types like maps or slices

Cache-line false sharing: when multiple atomic variables are stored in adjacent memory, a write to one invalidates the CPU cache line of another — degrading performance even though different variables are being modified. Pad hot atomic variables to separate cache lines (64 bytes on x86) in high-performance code.

What does atomic.CompareAndSwapInt64(&v, old, new) do?
Why prefer sync.Mutex over atomic when updating multiple related struct fields?
17. Implement a bounded worker pool pattern in Go.

A worker pool limits the number of goroutines working concurrently, preventing resource exhaustion (file handles, DB connections, memory) when processing a large number of tasks. It is one of the most commonly asked Go patterns in technical interviews.

type Job    struct { ID int; Payload string }
type Result struct { JobID int; Output string; Err error }

func workerPool(
    ctx        context.Context,
    jobs       <-chan Job,
    numWorkers int,
) <-chan Result {
    results := make(chan Result, numWorkers)
    var wg sync.WaitGroup
    wg.Add(numWorkers)

    for w := 0; w < numWorkers; w++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case job, ok := <-jobs:
                    if !ok { return } // channel closed — all work done
                    out, err := processJob(job)
                    select {
                    case results <- Result{job.ID, out, err}:
                    case <-ctx.Done(): return
                    }
                }
            }
        }()
    }

    go func() { wg.Wait(); close(results) }()
    return results
}

// Usage
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

jobs := make(chan Job, 100)
go func() {
    defer close(jobs)
    for i, item := range workItems {
        select {
        case jobs <- Job{ID: i, Payload: item}:
        case <-ctx.Done(): return
        }
    }
}()

for r := range workerPool(ctx, jobs, runtime.NumCPU()) {
    if r.Err != nil { log.Printf("job %d failed: %v", r.JobID, r.Err) }
}
How does the worker pool signal that all results have been produced?
For a CPU-bound workload, what is the optimal number of workers in a pool?
18. What are the differences between time.After, time.NewTimer, and time.NewTicker, and which leaks resources?

All three involve time-based channel operations but have distinct behaviours, reuse capabilities, and resource management responsibilities.

Time Facility Comparison
APIReturnsFiresResource leak riskReusable
time.After(d)<-chan TimeOnce, after dGoroutine + channel until d expires if case not takenNo
time.NewTimer(d)*time.TimerOnce, after dMust call Stop()Yes (via Reset)
time.NewTicker(d)*time.TickerEvery d foreverMUST call Stop() — goroutine lives until Stop or process exitYes (until stopped)
// time.After — convenient but leaks if the receive case is not taken
select {
case result := <-doWork():
    fmt.Println(result)
case <-time.After(5 * time.Second):
    fmt.Println("timeout")
    // If doWork wins: timer goroutine and channel live for 5s — avoid in loops
}

// time.NewTimer — correct approach for loops or where early stop is needed
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()  // always stop
select {
case result := <-doWork():
    timer.Stop()
    fmt.Println(result)
case <-timer.C:
    fmt.Println("timeout")
}

// time.NewTicker — periodic work, must stop
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()  // MUST stop — goroutine lives until Stop() called
for {
    select {
    case <-ticker.C:  doPeriodicWork()
    case <-ctx.Done(): return
    }
}

// Production: context.WithTimeout is preferred for request deadlines
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
Why should time.After be avoided inside a tight loop?
What must you do with a time.NewTicker when it is no longer needed?
19. How does context.Context enable clean goroutine cancellation and why is 'defer cancel()' critical?

context.Context is Go's standard mechanism for propagating cancellation, deadlines, and request-scoped values across API boundaries. Every blocking or long-running function should accept a context as its first parameter.

// Creating contexts
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // ALWAYS defer — releases resources even on the happy path

ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()

ctx3, cancel3 := context.WithDeadline(context.Background(),
    time.Now().Add(30*time.Second))
defer cancel3()

// Worker that respects cancellation
func worker(ctx context.Context, jobs <-chan Job) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err() // context.Canceled or context.DeadlineExceeded
        case job, ok := <-jobs:
            if !ok { return nil }
            if err := processWithCtx(ctx, job); err != nil { return err }
        }
    }
}

// Cancellation propagates through the context tree
parent, cancelParent := context.WithCancel(context.Background())
child1, _ := context.WithTimeout(parent, 10*time.Second)
child2, _ := context.WithCancel(parent)
cancelParent() // cancels parent, child1, AND child2 simultaneously

// What ctx.Err() returns:
// nil                           — context still active
// context.Canceled              — cancel() was called
// context.DeadlineExceeded      — deadline or timeout passed

Why always defer cancel(): failing to call cancel() leaks the goroutine that monitors the context for deadline expiry. This goroutine runs until either cancel() is called or the parent context is cancelled. In a high-traffic server, thousands of leaked monitor goroutines accumulate quickly.

What error does ctx.Err() return when a context is cancelled via the cancel() function?
What happens to a child context when its parent context is cancelled?
20. How do you use a buffered channel as a semaphore to limit goroutine concurrency?

A buffered channel of capacity N acts as a counting semaphore — at most N goroutines can be in a critical section simultaneously. This is a simple, idiomatic Go pattern for throttling concurrent HTTP requests, database queries, or any resource-bounded operation.

// Semaphore: limit to 10 concurrent HTTP fetches
func fetchAll(ctx context.Context, urls []string) []string {
    const maxConcurrent = 10
    sem := make(chan struct{}, maxConcurrent)

    results := make([]string, len(urls))
    var wg sync.WaitGroup

    for i, url := range urls {
        wg.Add(1)
        go func(idx int, u string) {
            defer wg.Done()

            // Acquire a slot (blocks if 10 goroutines are already running)
            select {
            case sem <- struct{}{}:
            case <-ctx.Done(): return
            }
            defer func() { <-sem }() // release slot on exit

            results[idx] = fetch(ctx, u)
        }(i, url)
    }

    wg.Wait()
    return results
}

// Why struct{} and not bool or int?
// struct{} is zero-size — no memory allocated for the value itself

// golang.org/x/sync/semaphore: weighted semaphore for varying costs
sem := semaphore.NewWeighted(10)
if err := sem.Acquire(ctx, 1); err != nil { return err }
defer sem.Release(1)
In the buffered-channel semaphore, what does sending to the semaphore channel represent?
Why is 'defer func() { <-sem }()' placed immediately after acquiring the semaphore?
21. What is the 'done channel' pattern and why has context.Context largely superseded it?

Before context.Context was standardised in Go 1.7, a chan struct{} was the primary way to broadcast a stop signal. Closing the channel (rather than sending) is used because it unblocks all waiting receivers simultaneously — a broadcast, not a point-to-point signal.

// Done channel pattern (pre-1.7 idiom)
done := make(chan struct{})

func startWorker(done <-chan struct{}, jobs <-chan Job) {
    for {
        select {
        case <-done: return          // all goroutines unblock at once
        case job := <-jobs: process(job)
        }
    }
}

for i := 0; i < 5; i++ { go startWorker(done, jobs) }
close(done) // broadcasts stop to all 5 workers in O(1)

// Why close() instead of sending N values?
// close() is O(1) and unblocks ALL receivers simultaneously.
// Sending N values requires knowing N and sending N times.

// Why context.Context is preferred today:
// 1. Propagates deadlines/timeouts automatically
// 2. Carries the reason: ctx.Err() returns Canceled or DeadlineExceeded
// 3. Standard API — all stdlib network/IO functions accept context
// 4. No manual channel plumbing through every function signature

// Modern equivalent:
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 5; i++ {
    go func() {
        for {
            select {
            case <-ctx.Done(): return
            case job := <-jobs: process(job)
            }
        }
    }()
}
cancel() // equivalent to close(done)
Why is close(done) preferred over sending N values to stop N goroutines?
What capability does context.Context add that a plain done channel lacks?
22. What is the goroutine loop-variable capture bug and how do you fix it?

One of the most common Go concurrency interview puzzles. When a goroutine closure captures a loop variable by reference, all goroutines in the loop share the same variable — which has already advanced to its final value by the time any goroutine runs.

// BUG (Go 1.21 and earlier): all goroutines may print 5
for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i) // captures &i — reads whatever i is RIGHT NOW
    }()
}
// By the time goroutines execute, the loop finished and i == 5
// Typical output: 5 5 5 5 5

// FIX 1: pass as argument — creates an independent copy per iteration
for i := 0; i < 5; i++ {
    go func(n int) { // n is a local copy of i at this point
        fmt.Println(n) // 0 1 2 3 4 (in any order)
    }(i)
}

// FIX 2: shadow the loop variable inside the loop body
for i := 0; i < 5; i++ {
    i := i // new variable, shadows the outer i
    go func() { fmt.Println(i) }()
}

// GO 1.22+: loop variables are per-iteration by default
// for i := range 5 { go func() { fmt.Println(i) }() } // safe in 1.22!

// Same bug with range over slice
names := []string{"Alice", "Bob", "Carol"}
for _, name := range names {
    go func() { fmt.Println(name) }() // BUG: all may print "Carol"
}

// Fix
for _, name := range names {
    go func(n string) { fmt.Println(n) }(name)
}
Why do goroutines launched in a for loop often all print the loop's final value?
What changed in Go 1.22 that eliminates this bug for range loops?
23. How does golang.org/x/sync/errgroup simplify concurrent error handling?

errgroup.Group is a higher-level abstraction over sync.WaitGroup that adds automatic error collection and optional context cancellation. It is the idiomatic tool for the pattern of running N goroutines and returning the first non-nil error.

import "golang.org/x/sync/errgroup"

// Basic errgroup — collect first error
func fetchAll(urls []string) error {
    g := new(errgroup.Group)
    for _, url := range urls {
        url := url // capture for goroutine (pre-Go 1.22)
        g.Go(func() error {
            resp, err := http.Get(url)
            if err != nil { return err }
            defer resp.Body.Close()
            return nil
        })
    }
    return g.Wait() // waits for all; returns first non-nil error
}

// errgroup with context — cancel all on first error
func fetchWithCancel(ctx context.Context, urls []string) error {
    g, gctx := errgroup.WithContext(ctx)
    // gctx is cancelled when any goroutine returns an error

    for _, url := range urls {
        url := url
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(gctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil { return err }
            defer resp.Body.Close()
            return nil
        })
    }
    return g.Wait()
}

// Limit concurrency (Go 1.20+)
g.SetLimit(10) // max 10 goroutines — g.Go blocks when at limit
for _, url := range urls {
    url := url
    g.Go(func() error { return fetch(url) })
}
What does errgroup.Group.Wait() return?
What does errgroup.WithContext(ctx) provide that a plain errgroup.Group does not?
24. When should a channel carry 'chan struct{}' versus a typed value, and why is close() used for broadcast?

chan struct{} is the Go idiom for pure signalling — when the fact that something occurred matters, but no data needs to be transferred. struct{}{} has zero size (no memory allocated for the value), and using it explicitly communicates 'this is a signal only'.

// chan struct{} — pure event signal (no payload)
done := make(chan struct{})
go func() {
    doWork()
    close(done) // broadcast: no value needed
}()
<-done // wait for signal

// Buffered chan struct{} as semaphore (acquire/release)
sem := make(chan struct{}, 10)
sem <- struct{}{}   // acquire
<-sem               // release

// chan bool — avoid for signals (true vs false ambiguity)
// What does 'false' mean? Failure? Not-yet? Done? Confusing.

// chan T — use when data has semantic meaning
results := make(chan int, 100)  // carries computed results
errors  := make(chan error, 10) // carries error values

// Broadcast via close: O(1), unblocks ALL receivers
ready := make(chan struct{})
go func() {
    initialize()
    close(ready) // all waiting goroutines unblock simultaneously
}()

for i := 0; i < 5; i++ {
    go func() {
        <-ready      // all 5 unblock at the instant close() is called
        doPostInit()
    }()
}

// struct{} size verification
fmt.Println(unsafe.Sizeof(struct{}{})) // 0 bytes
What advantage does 'chan struct{}' have over 'chan bool' for signalling?
Why is closing a chan struct{} the most efficient broadcast mechanism?
25. What is GOMAXPROCS, how does it affect parallelism, and what is the container pitfall?

GOMAXPROCS controls the number of OS threads (Ps) that can execute Go code simultaneously. It defaults to runtime.NumCPU() — the number of logical CPU cores on the host. Misunderstanding its default is one of the most common production performance issues for containerised Go services.

import "runtime"

// Query current GOMAXPROCS (0 = query without changing)
fmt.Println(runtime.GOMAXPROCS(0))

// Set programmatically (returns previous value)
prev := runtime.GOMAXPROCS(4)

// Or via environment variable before process starts:
// GOMAXPROCS=4 ./myapp

// CPU-bound: increasing GOMAXPROCS → more true parallelism
// I/O-bound: GOMAXPROCS matters less — blocked goroutines park and
//            release their P for other goroutines to use

// ── THE CONTAINER PITFALL ──
// Default: GOMAXPROCS = host CPU count (e.g. 64 on a 64-core machine)
// Container CPU quota: 0.5 CPU
// Result: 64 OS threads compete for 0.5 CPU time slice
// → excessive context switching, scheduler overhead, latency spikes

// FIX: uber-go/automaxprocs — reads cgroup CPU quota automatically
// import _ "go.uber.org/automaxprocs"
// Sets GOMAXPROCS = ceil(containerCPUQuota) at startup

// Or set manually in your container entrypoint:
// GOMAXPROCS=$(nproc --ignore=0) ./myapp

// Scheduler tracing:
// GODEBUG=schedtrace=1000 ./myapp  → prints stats every 1s

GOMAXPROCS=1: goroutines interleave but never truly run in parallel. Useful for reproducing certain race conditions that only appear with true parallelism, or for testing that code is correct regardless of scheduling order. Never use GOMAXPROCS=1 as a race condition fix — use -race.

What is the container pitfall with Go's default GOMAXPROCS setting?
Which library automatically corrects GOMAXPROCS for containerised Go services?
26. How does Go's select handle multiple ready cases, and how do you implement true priority?

When multiple cases in a select are simultaneously ready, Go picks one uniformly at random. This prevents deterministic starvation but does not guarantee priority. Implementing true priority requires nested selects or a dedicated check before the main select.

// Standard select: random among ready cases
// Even if ctx is cancelled AND a job is ready, ~50% of the time the job wins
for {
    select {
    case <-ctx.Done(): return
    case job := <-jobs: process(job)
    }
}

// Priority select: always check cancellation first
// Step 1: fast non-blocking check of the high-priority case
for {
    select {
    case <-ctx.Done(): return   // check first: if done, exit immediately
    default:                    // not done: fall through to main select
    }

    // Step 2: main blocking select
    select {
    case <-ctx.Done(): return
    case job := <-jobs: process(job)
    }
}

// True channel priority (hi > lo): nested select pattern
func drainWithPriority(hi, lo <-chan int) {
    for {
        // Always drain hi before touching lo
        select {
        case v := <-hi: handle(v); continue
        default:
        }
        // hi empty: process either
        select {
        case v := <-hi: handle(v)
        case v := <-lo: handle(v)
        }
    }
}
When multiple cases in a select statement are simultaneously ready, which one executes?
How do you implement priority in a select — ensuring a high-priority channel is always checked first?
27. What is sync.Cond and when do you use it instead of channels?

sync.Cond is a condition variable — a synchronisation primitive for goroutines to wait for or announce a state change on a shared resource. Use it when: many goroutines wait for a complex shared-state predicate; waking all waiters at once is needed; or channels would add unnecessary data-passing overhead.

// Bounded buffer using sync.Cond
type BoundedQueue struct {
    mu       sync.Mutex
    notFull  *sync.Cond
    notEmpty *sync.Cond
    items    []any
    capacity int
}

func NewBoundedQueue(cap int) *BoundedQueue {
    q := &BoundedQueue{capacity: cap}
    q.notFull  = sync.NewCond(&q.mu)
    q.notEmpty = sync.NewCond(&q.mu)
    return q
}

func (q *BoundedQueue) Put(item any) {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == q.capacity { // LOOP — never if! (spurious wakeups)
        q.notFull.Wait()             // atomically releases lock and sleeps
    }
    q.items = append(q.items, item)
    q.notEmpty.Signal() // wake one waiting consumer
}

func (q *BoundedQueue) Get() any {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.notEmpty.Wait()
    }
    item := q.items[0]
    q.items = q.items[1:]
    q.notFull.Signal()
    return item
}

// cond.Signal()    — wake ONE goroutine (when only one should act)
// cond.Broadcast() — wake ALL goroutines (when state change affects all)

Key rule: always check the condition in a for loop, never an if. cond.Wait() can return spuriously (OS wakeup without Signal/Broadcast), so the condition must be re-verified after each wakeup. Wait() atomically releases the associated mutex and sleeps; the mutex is re-acquired before returning.

Why must the condition predicate be checked in a 'for' loop, not an 'if', after sync.Cond.Wait()?
What is the difference between cond.Signal() and cond.Broadcast()?
28. Implement Go's canonical pipeline pattern with cancellation from the Go blog.

The Go blog defines three-stage pipelines: a generator that produces values, one or more transformation stages, and a consumer — all connected by directional channels, with cancellation via context.

// Stage 1: Generator
func generate(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
}

// Stage 2: Transform
func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for v := range in {
            select {
            case out <- v * v:
            case <-ctx.Done(): return
            }
        }
    }()
    return out
}

// Stage 3: Filter
func filterAbove(ctx context.Context, in <-chan int, min int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for v := range in {
            if v <= min { continue }
            select {
            case out <- v:
            case <-ctx.Done(): return
            }
        }
    }()
    return out
}

// Consumer
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    nums    := generate(ctx, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    squares := square(ctx, nums)
    results := filterAbove(ctx, squares, 25)

    for i := 0; i < 3; i++ { fmt.Println(<-results) }
    cancel() // stops all upstream stages cleanly
}
What happens to upstream pipeline stages when the consumer calls cancel()?
Why does each pipeline stage close its output channel with 'defer close(out)'?
29. What is Go's memory model and why does it matter for concurrent code?

Go's memory model defines which memory operations in one goroutine are guaranteed to be visible to operations in another. Without understanding it, concurrent code may appear to work correctly in tests but fail silently in production under different compiler optimisations or CPU architectures.

Key Happens-Before Guarantees
OperationGuarantee
go f()All operations before the go statement happen-before f() starts executing
Goroutine exitNOT automatically visible — use WaitGroup, channel, or other sync
ch <- v (send)The send completes before the corresponding receive returns
close(ch)The close happens-before any receive that returns the zero value
mu.Unlock()The nth Unlock() happens-before the (n+1)th Lock() on the same mutex
once.Do(f)The single execution of f happens-before any once.Do() returns
// WRONG: no happens-before between goroutine exit and main reading data
var data string
go func() { data = "hello" }()
// time.Sleep(time.Second)  // WRONG: Sleep is not a sync primitive
fmt.Println(data)           // may see empty string — data race!

// CORRECT: channel close establishes happens-before
done := make(chan struct{})
go func() {
    data = "hello"   // write
    close(done)      // happens-before any <-done returns
}()
<-done               // happens-after close(done) → sees data = "hello"
fmt.Println(data)    // guaranteed: "hello"

// WHY Sleep fails:
// The CPU or compiler may keep data in a register.
// Sleep does not cause any memory barrier.
// Only explicit synchronisation primitives create happens-before edges.
Why is using time.Sleep() to wait for a goroutine's memory writes incorrect?
What happens-before relationship does closing a channel establish?
30. What is the check-then-act race condition (TOCTOU) and how do you fix it?

The Time Of Check, Time Of Use (TOCTOU) race: a goroutine checks a condition, releases the lock, then acts — but between check and act another goroutine changes the condition. The fix is to hold the lock for the entire check-and-act sequence, or use an atomic compare-and-swap.

// BUGGY: check-then-act race — lock released between check and act
type Cache struct {
    mu    sync.Mutex
    items map[string]string
}

func (c *Cache) GetOrComputeBuggy(key string, compute func() string) string {
    c.mu.Lock()
    val, ok := c.items[key] // CHECK
    c.mu.Unlock()
    if ok { return val }
    // ← Another goroutine may compute and cache here
    result := compute()      // expensive — no lock held
    c.mu.Lock()
    c.items[key] = result   // ACT — may overwrite another goroutine's result
    c.mu.Unlock()
    return result
}

// FIX 1: singleflight — one computation per key at a time
import "golang.org/x/sync/singleflight"

var group singleflight.Group

func (c *Cache) GetOrComputeSF(key string, compute func() string) string {
    v, _, _ := group.Do(key, func() (any, error) {
        c.mu.Lock()
        if val, ok := c.items[key]; ok {
            c.mu.Unlock()
            return val, nil
        }
        c.mu.Unlock()
        result := compute()
        c.mu.Lock()
        c.items[key] = result
        c.mu.Unlock()
        return result, nil
    })
    return v.(string)
}

// FIX 2: sync.Map.LoadOrStore — atomic check-and-store
var sm sync.Map
actual, loaded := sm.LoadOrStore(key, expensiveValue)
// CAUTION: expensiveValue is computed before the call regardless
What is the TOCTOU race condition?
What does singleflight.Group.Do(key, fn) guarantee?
31. What is asynchronous preemption in Go (1.14+) and why was it introduced?

Before Go 1.14, goroutine scheduling was cooperative: a goroutine only yielded its processor at specific safe points — function call sites, channel operations, and syscalls. A CPU-bound goroutine in a tight loop with no function calls could starve other goroutines indefinitely and block GC stop-the-world phases.

// Pre-1.14: this goroutine could starve all others on its P indefinitely
go func() {
    for {
        x := 0
        for i := 0; i < 1_000_000_000; i++ { x++ }
        // No function calls → no scheduling point
        // Other goroutines on this P cannot run
        // GC STW cannot proceed — GC pause stretches indefinitely
    }
}()

// Go 1.14+: asynchronous preemption via SIGURG
// sysmon goroutine detects a goroutine running on a P for > 10ms
// It sends SIGURG to the OS thread running that goroutine
// The signal handler inserts a preemption point; the goroutine yields

// Effect:
// - Tight loops no longer starve other goroutines
// - GC STW completes in bounded time regardless of goroutine behaviour
// - Programs are more responsive under CPU-heavy workloads

// runtime.Gosched() — explicit cooperative yield (still useful)
for i := 0; i < 1_000_000; i++ {
    doHeavyChunk(i)
    if i%1000 == 0 {
        runtime.Gosched() // voluntarily yield every 1000 iterations
    }
}

// Preemption safety: goroutine stacks may be moved during preemption
// → All stack references must be valid Go pointers (enforces unsafe.Pointer rules)
What mechanism does Go 1.14+ use to preempt goroutines stuck in tight CPU-bound loops?
What is runtime.Gosched() used for?
32. How do you safely use a map from multiple goroutines in Go?

Go maps are not safe for concurrent use. The runtime detects concurrent map access and throws a fatal error: 'concurrent map read and map write'. There are three main solutions, each with different performance trade-offs.

// BUGGY: concurrent map access — runtime fatal error
var m = map[string]int{}
for i := 0; i < 100; i++ {
    go func(n int) {
        m[fmt.Sprint(n)] = n // RACE: concurrent write
    }(i)
}

// SOLUTION 1: sync.Mutex protecting a regular map
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}
func (s *SafeMap) Set(k string, v int) {
    s.mu.Lock(); defer s.mu.Unlock()
    s.m[k] = v
}
func (s *SafeMap) Get(k string) (int, bool) {
    s.mu.RLock(); defer s.mu.RUnlock()
    v, ok := s.m[k]
    return v, ok
}

// SOLUTION 2: sync.Map (optimised for read-heavy / disjoint-key patterns)
var sm sync.Map
sm.Store("key", 42)
val, ok := sm.Load("key")
sm.Delete("key")
sm.Range(func(k, v any) bool {
    fmt.Println(k, v)
    return true // return false to stop
})

// SOLUTION 3: actor model — single goroutine owns the map
type op struct{ key string; val int; resp chan int }
opCh := make(chan op)
go func() {
    m := map[string]int{}
    for o := range opCh {
        m[o.key] = o.val
        if o.resp != nil { o.resp <- m[o.key] }
    }
}()
What error does the Go runtime throw when two goroutines access a map concurrently with at least one write?
When is sync.Map preferred over a mutex-protected map?
33. How do you use a buffered channel as a task queue with natural backpressure?

A buffered channel provides natural backpressure: the producer blocks when the queue is full, signalling the consumer cannot keep up. This prevents unbounded memory growth without any additional data structure and is idiomatic Go.

type TaskQueue struct {
    tasks chan func()
    quit  chan struct{}
}

func NewTaskQueue(bufSize, workers int) *TaskQueue {
    q := &TaskQueue{
        tasks: make(chan func(), bufSize), // bounded queue
        quit:  make(chan struct{}),
    }
    for w := 0; w < workers; w++ { go q.worker() }
    return q
}

func (q *TaskQueue) worker() {
    for {
        select {
        case <-q.quit: return
        case task := <-q.tasks: task()
        }
    }
}

// Submit blocks when queue is full — natural backpressure
func (q *TaskQueue) Submit(ctx context.Context, task func()) error {
    select {
    case q.tasks <- task:
        return nil
    case <-ctx.Done():
        return ctx.Err() // caller's deadline expired waiting for a slot
    }
}

// TrySubmit — non-blocking: drop if queue is full
func (q *TaskQueue) TrySubmit(task func()) bool {
    select {
    case q.tasks <- task: return true
    default:              return false
    }
}

func (q *TaskQueue) Shutdown() { close(q.quit) }
What happens when Submit() is called and the task queue channel is full?
How does TrySubmit() differ from Submit()?
34. How does Go handle goroutines that make blocking syscalls — what happens to M and P?

When a goroutine makes a blocking OS syscall (file I/O, sleep), Go must not stall its P — other goroutines need to continue running. The runtime handles this through handoff: the P detaches from the blocked M and continues execution on a new M.

// What happens when goroutine G1 calls os.ReadFile (blocking syscall):

// BEFORE SYSCALL:
// P1 → M1 (running G1), G2/G3 in P1's local queue

// ENTERING SYSCALL:
// 1. M1 enters the OS kernel for the syscall
// 2. Runtime detects M1 is in a syscall (can't schedule on M1)
// 3. P1 detaches from M1
// 4. P1 acquires or creates a new M (M2)
// 5. M2+P1 continues running G2, G3 ...
// 6. M1 is a blocked kernel thread — parked in the OS

// AFTER SYSCALL COMPLETES:
// 7. M1 returns from the kernel; G1 is now runnable
// 8. M1 tries to acquire any idle P
// 9a. P available  → M1+P runs G1
// 9b. No P idle    → G1 to global run queue; M1 parks

// WHY NET I/O SCALES BETTER:
// Go wraps all socket I/O in non-blocking syscalls + epoll/kqueue (netpoller)
// A goroutine doing net read → parks immediately (no M blocked in kernel)
// When data arrives → epoll wakes → goroutine added to run queue
// → Net goroutines hold no M while waiting for I/O

// Practical consequence:
// net/http server: 100K concurrent connections with ~GOMAXPROCS threads
// File I/O: each blocking os.ReadFile ties up an M — scale carefully
When a goroutine blocks in a syscall, what happens to its P?
Why does Go's network I/O scale better than file I/O for high-concurrency workloads?
35. Implement a simple publish-subscribe broker using Go channels.

A pub-sub system decouples publishers from subscribers. The idiomatic Go implementation uses a broker goroutine that owns the subscriber registry and distributes messages — a single goroutine owning a map eliminates all locking.

type Broker[T any] struct {
    publish   chan T
    subscribe chan chan<- T
    cancel    chan chan<- T
    quit      chan struct{}
}

func NewBroker[T any]() *Broker[T] {
    b := &Broker[T]{
        publish:   make(chan T, 64),
        subscribe: make(chan chan<- T),
        cancel:    make(chan chan<- T),
        quit:      make(chan struct{}),
    }
    go b.run()
    return b
}

func (b *Broker[T]) run() {
    subs := map[chan<- T]struct{}{}
    for {
        select {
        case <-b.quit:
            for sub := range subs { close(sub) }
            return
        case sub := <-b.subscribe:
            subs[sub] = struct{}{}
        case sub := <-b.cancel:
            delete(subs, sub); close(sub)
        case msg := <-b.publish:
            for sub := range subs {
                select {
                case sub <- msg: // non-blocking: slow subscribers may miss
                default:
                }
            }
        }
    }
}

func (b *Broker[T]) Publish(v T)         { b.publish <- v }
func (b *Broker[T]) Subscribe() <-chan T {
    ch := make(chan T, 16)
    b.subscribe <- ch
    return ch
}
func (b *Broker[T]) Stop() { close(b.quit) }
Why are sends to each subscriber non-blocking (using default) in the broker's run loop?
Why does the broker use a single goroutine owning the 'subs' map instead of a mutex?
36. What is the lock-held-during-I/O anti-pattern and how do you fix it?

Holding a mutex while performing I/O (network calls, disk reads) serialises all goroutines that need that lock for the entire I/O duration. This collapses concurrency to near-zero throughput — one of the most common Go performance mistakes.

// ANTI-PATTERN: lock held during HTTP call
type Cache struct {
    mu    sync.Mutex
    items map[string]string
}

func (c *Cache) GetBad(key string) string {
    c.mu.Lock()
    defer c.mu.Unlock()
    if v, ok := c.items[key]; ok { return v }
    // LOCK HELD DURING NETWORK CALL — all other callers serialise here!
    resp, _ := http.Get("https://api.example.com/" + key)
    body, _ := io.ReadAll(resp.Body)
    c.items[key] = string(body)
    return c.items[key]
}

// CORRECT: release lock before I/O, re-acquire after
func (c *Cache) GetGood(key string) string {
    // Phase 1: fast check under lock
    c.mu.Lock()
    if v, ok := c.items[key]; ok {
        c.mu.Unlock()
        return v
    }
    c.mu.Unlock()

    // Phase 2: expensive I/O WITHOUT the lock
    resp, _ := http.Get("https://api.example.com/" + key)
    body, _ := io.ReadAll(resp.Body)
    result := string(body)

    // Phase 3: store result under lock
    c.mu.Lock()
    c.items[key] = result
    c.mu.Unlock()
    return result
}

// Even better: use singleflight to deduplicate concurrent fetches for the same key
What is the performance problem with holding a sync.Mutex while making an HTTP request?
What is the correct strategy when a critical section requires expensive I/O?
37. Write a complete example of implementing operation timeouts in Go using select.

Timeouts are critical for preventing goroutine leaks in service calls. The idiomatic Go approach uses context.WithTimeout (production preferred) or time.After (quick one-off). A crucial detail: the result channel must be buffered to avoid a goroutine leak when the timeout fires first.

// Pattern 1: context.WithTimeout (production preferred)
func callService(ctx context.Context, req Request) (Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // ALWAYS: releases resources even on happy path

    result := make(chan Response, 1) // buffered — prevents goroutine leak!
    errCh  := make(chan error, 1)

    go func() {
        resp, err := doServiceCall(ctx, req) // honours context cancellation
        if err != nil { errCh <- err; return }
        result <- resp
    }()

    select {
    case resp := <-result: return resp, nil
    case err  := <-errCh:  return Response{}, err
    case <-ctx.Done():     return Response{}, fmt.Errorf("service: %w", ctx.Err())
    }
}

// Pattern 2: time.After (simpler for standalone use)
func computeWithTimeout(input int) (int, error) {
    result := make(chan int, 1) // MUST be buffered — see below
    go func() {
        result <- expensiveCompute(input)
    }()
    select {
    case v := <-result:
        return v, nil
    case <-time.After(3 * time.Second):
        // The goroutine is NOT killed — it still runs to completion
        // Buffered channel lets it send and exit without blocking
        return 0, errors.New("computation timed out")
    }
}

// WHY unbuffered channel leaks:
// result := make(chan int)   ← unbuffered
// If timeout fires first, the goroutine blocks forever on 'result <-'
// (nobody receives) → GOROUTINE LEAK
Why must the result channel be buffered when using time.After for a timeout?
Why is context.WithTimeout preferred over time.After in production service code?
38. How do you write tests that detect goroutine leaks automatically?

Goroutine leaks are among the hardest production bugs to diagnose — they accumulate invisibly. Catching them at test time is far more effective than debugging production memory growth.

import (
    "testing"
    "runtime"
    "time"
    "go.uber.org/goleak"
)

// Method 1: goleak — recommended, most accurate
func TestNoLeak(t *testing.T) {
    defer goleak.VerifyNone(t) // fails if goroutines remain after test

    svc := NewService()
    svc.Start()
    // ... exercise the service ...
    svc.Stop() // if Stop() leaks a goroutine, goleak catches it
}

// goleak with TestMain — check all tests in the package
func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m,
        goleak.IgnoreTopFunction("database/sql.(*DB).connectionOpener"),
    )
}

// Method 2: manual count check (simpler, less precise)
func TestGoroutineCount(t *testing.T) {
    before := runtime.NumGoroutine()

    svc := NewService()
    svc.Start()
    svc.Stop()

    time.Sleep(100 * time.Millisecond) // let goroutines exit
    runtime.Gosched()

    after := runtime.NumGoroutine()
    if after > before {
        buf := make([]byte, 1<<20)
        n := runtime.Stack(buf, true)
        t.Errorf("goroutine leak: %d before, %d after\n%s",
            before, after, buf[:n])
    }
}
What does goleak.VerifyNone(t) check when called via defer?
Why is time.Sleep(100ms) used in the manual goroutine count check?
39. Implement a concurrent word count across multiple files — a classic Go interview puzzle.

This exercise tests goroutine spawning, WaitGroup usage, channel fan-in, and safe result aggregation. It is a common live-coding assignment in Go technical screens.

package main

import (
    "bufio"
    "context"
    "fmt"
    "os"
    "sync"
)

type FileCount struct {
    File  string
    Words int
    Err   error
}

func countWords(ctx context.Context, path string) FileCount {
    f, err := os.Open(path)
    if err != nil { return FileCount{File: path, Err: err} }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    scanner.Split(bufio.ScanWords)
    count := 0
    for scanner.Scan() {
        select {
        case <-ctx.Done(): return FileCount{File: path, Err: ctx.Err()}
        default:
        }
        count++
    }
    return FileCount{File: path, Words: count, Err: scanner.Err()}
}

func parallelWordCount(ctx context.Context, files []string) (int, []error) {
    results := make(chan FileCount, len(files)) // buffered — no goroutine blocks

    var wg sync.WaitGroup
    for _, f := range files {
        wg.Add(1)
        go func(path string) {
            defer wg.Done()
            results <- countWords(ctx, path)
        }(f)
    }

    go func() { wg.Wait(); close(results) }()

    total := 0
    var errs []error
    for r := range results {
        if r.Err != nil {
            errs = append(errs, fmt.Errorf("%s: %w", r.File, r.Err))
            continue
        }
        total += r.Words
    }
    return total, errs
}
Why is the 'results' channel buffered with capacity len(files)?
Why must close(results) be called inside a separate goroutine waiting on the WaitGroup?
40. How does GOMAXPROCS=1 change behaviour and when is it actually useful?

With GOMAXPROCS=1, only one goroutine executes at any instant — goroutines interleave cooperatively but never run truly in parallel. This changes the scheduling behaviour but does not eliminate race conditions.

runtime.GOMAXPROCS(1)

// With GOMAXPROCS=1: only one goroutine runs at a time
// There is no true parallelism
// Some races become less likely to manifest — but they still exist!

// WRONG: GOMAXPROCS=1 does NOT eliminate races
// Cooperative preemption at function calls still allows interleaving
var x int
go func() {
    x++           // goroutine may yield here
    fmt.Println(x) // x may have changed between ++ and Println
}()

// ALWAYS use -race flag — not GOMAXPROCS=1 — to detect races

// Legitimate uses of GOMAXPROCS=1:
// 1. Reproduce bugs that require specific goroutine ordering
// 2. Test that code produces correct results without parallelism
//    (if code only works with multiple Ps, that's a design flaw)
// 3. Performance comparison: scheduling overhead vs pure compute

// Testing sequential correctness:
func TestSequential(t *testing.T) {
    prev := runtime.GOMAXPROCS(1)
    defer runtime.GOMAXPROCS(prev) // restore after test

    result := concurrentAlgorithm([]int{1, 2, 3, 4, 5})
    if result != 15 {
        t.Errorf("expected 15, got %d", result)
    }
}
Does GOMAXPROCS=1 guarantee that your code has no race conditions?
What is a legitimate use of GOMAXPROCS=1 in tests?
41. How do you implement a high-performance sharded concurrent map in Go?

A single mutex protecting one map is a bottleneck under high concurrency. A sharded map divides the key space across N independent maps, each with its own mutex, reducing contention by approximately N-fold. Goroutines with keys in different shards can operate in parallel without competing.

import (
    "hash/fnv"
    "fmt"
    "sync"
)

const numShards = 32

type ShardedMap[K comparable, V any] struct {
    shards [numShards]struct {
        sync.RWMutex
        m map[K]V
    }
}

func NewShardedMap[K comparable, V any]() *ShardedMap[K, V] {
    sm := &ShardedMap[K, V]{}
    for i := range sm.shards { sm.shards[i].m = make(map[K]V) }
    return sm
}

func (sm *ShardedMap[K, V]) shardFor(key K) *struct {
    sync.RWMutex; m map[K]V
} {
    h := fnv.New32a()
    fmt.Fprint(h, key)
    return &sm.shards[h.Sum32()%numShards]
}

func (sm *ShardedMap[K, V]) Set(key K, val V) {
    s := sm.shardFor(key)
    s.Lock(); defer s.Unlock()
    s.m[key] = val
}

func (sm *ShardedMap[K, V]) Get(key K) (V, bool) {
    s := sm.shardFor(key)
    s.RLock(); defer s.RUnlock()
    v, ok := s.m[key]
    return v, ok
}

// With 32 shards: 32 goroutines with different keys can write simultaneously
// vs a single mutex where all goroutines serialise into one queue
How does a sharded map reduce lock contention compared to a single mutex map?
What determines which shard a key maps to in a sharded map?
42. What are the specific happens-before guarantees for channel operations in Go's memory model?

Go's memory model specifies precise happens-before rules for channels. Knowing these is necessary for writing correct concurrent code that works across CPU architectures without data races.

Channel Happens-Before Rules
OperationGuarantee
Send on a channelCompletes before the receive from that send returns
Close of a channelHappens-before a receive that returns the zero value (closed-channel read)
Receive from unbuffered channelHappens-before the send on that channel completes
kth receive from buffered (cap=C)Happens-before the (k+C)th send completes — enables semaphore semantics
// Rule 1: send completes before receive returns
var data string
ch := make(chan struct{})
go func() {
    data = "shared"  // write
    ch <- struct{}{} // send: completes before <-ch returns
}()
<-ch               // receive: data write is guaranteed visible
fmt.Println(data)  // "shared" — safe

// Rule 2: close happens-before zero-value receive
var ready bool
done := make(chan struct{})
go func() { ready = true; close(done) }()
<-done
fmt.Println(ready) // guaranteed: true

// Rule 3: unbuffered — receive happens-before send completes
// (sender cannot proceed until receiver has the value)

// Rule 4: buffered channel as semaphore
// cap=1 channel: 1st receive happens-before 2nd send completes
limit := make(chan struct{}, 1)
var shared int
go func() {
    limit <- struct{}{} // 1st send
    shared = 42
    <-limit            // 1st receive: happens-before 2nd send
}()
go func() {
    limit <- struct{}{} // 2nd send — cannot complete until 1st recv done
    fmt.Println(shared) // guaranteed to see 42
    <-limit
}()
What happens-before relationship does closing a channel establish?
What property does 'kth receive happens-before (k+C)th send' enable for a buffered channel of capacity C?
43. How do you implement a hedged request pattern using select and goroutines?

A hedged request sends the same request to multiple backends simultaneously and returns the first successful response, cancelling the rest. It trades slightly higher resource use for dramatically lower tail latency — a technique from Google's Bigtable paper.

func hedgedFetch(ctx context.Context, urls []string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // cancels all in-flight requests when we return

    type result struct{ data []byte; err error }
    ch := make(chan result, len(urls)) // buffered — goroutines can exit freely

    for _, url := range urls {
        url := url
        go func() {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil { ch <- result{err: err}; return }
            defer resp.Body.Close()
            data, err := io.ReadAll(resp.Body)
            ch <- result{data: data, err: err}
        }()
    }

    var errs []error
    for i := 0; i < len(urls); i++ {
        r := <-ch
        if r.err == nil {
            cancel() // cancel remaining requests
            return r.data, nil
        }
        errs = append(errs, r.err)
    }
    return nil, fmt.Errorf("all backends failed: %v", errs)
}

// Timed hedging — send to fallback only after 100ms
func timedHedge(ctx context.Context, primary, fallback string) ([]byte, error) {
    ch := make(chan result, 2)
    go func() { ch <- fetch(ctx, primary) }()
    select {
    case r := <-ch:
        if r.err == nil { return r.data, nil }
    case <-time.After(100 * time.Millisecond):
        go func() { ch <- fetch(ctx, fallback) }()
    }
    for i := 0; i < 2; i++ {
        if r := <-ch; r.err == nil { return r.data, nil }
    }
    return nil, errors.New("all requests failed")
}
Why is the result channel buffered with capacity len(urls) in the hedged request?
What does cancel() achieve when called after receiving the first successful response?
44. How does sync.Pool reduce GC pressure in high-throughput Go services?

sync.Pool is a concurrent pool of reusable temporary objects. By returning objects to the pool after use instead of discarding them, allocation and GC pressure are significantly reduced — critical for high-throughput services like HTTP servers and JSON encoders.

// Pool of byte buffers reused per request
var bufPool = sync.Pool{
    New: func() any {
        buf := make([]byte, 0, 4096)
        return &buf
    },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    bufPtr := bufPool.Get().(*[]byte) // may be new or recycled
    buf := (*bufPtr)[:0]              // reset length, keep capacity

    // Use buf as scratch space...
    buf = append(buf, buildResponse(r)...)
    w.Write(buf)

    // Return to pool — ALWAYS reset state first!
    *bufPtr = buf
    bufPool.Put(bufPtr)
}

// KEY RULES:
// 1. GC MAY drain the pool at any time — pool items are NOT permanent
// 2. Get() returns a new object (via New) or a recycled one — both OK
// 3. ALWAYS reset state before Put() — recycled objects carry old data
// 4. Never use a pooled object after Put() — it belongs to the pool
// 5. All objects in the pool must be the same type

// Real usage:
// - encoding/json uses sync.Pool for encoder/decoder state
// - net/http uses it for request/response buffers
// - fmt uses it for print state (pp struct)

// Benchmarks show 40%+ reduction in allocations under sustained load
What happens to items in sync.Pool when the garbage collector runs?
Why must you reset an object's state before calling Put() to return it to sync.Pool?
45. What is a livelock and how does it differ from a deadlock in Go programs?

A deadlock: all goroutines are blocked — no progress is possible. A livelock: all goroutines are actively running but constantly reacting to each other in a loop that prevents any meaningful progress — like two people in a corridor who keep stepping in the same direction to let the other pass.

// DEADLOCK: goroutines blocked — Go runtime detects it
ch := make(chan int)
ch <- 1  // send blocks, no receiver → runtime: 'all goroutines asleep'

// LIVELOCK: goroutines running but making no net progress
type lock struct{ taken bool }

func acquirePolite(own, other *lock) {
    for {
        own.taken = true
        time.Sleep(time.Millisecond)
        if other.taken {     // the other goroutine also has its lock
            own.taken = false // politely give up and retry → infinite loop!
            time.Sleep(time.Millisecond)
            continue
        }
        other.taken = true
        break // rarely reached
    }
}

a, b := &lock{}, &lock{}
go acquirePolite(a, b) // both goroutines busy but stuck
go acquirePolite(b, a)

// Go runtime does NOT detect livelocks — goroutines are 'running'

// Detection:
// - CPU at 100% with no observable progress
// - pprof CPU profile shows same functions in an infinite spin
// - go tool trace shows goroutines executing but state never advancing

// Prevention:
// - Randomised exponential backoff: rand.Sleep between retries
// - Consistent lock-acquisition ordering
// - Prefer context-based timeouts over spin-wait patterns
What is the key difference between a deadlock and a livelock?
Why does the Go runtime detect deadlocks but not livelocks?
46. How do you implement backpressure in Go to prevent overloading downstream systems?

Backpressure is the mechanism by which a slow consumer signals a fast producer to slow down. Without it, producers overwhelm consumers, causing unbounded queue growth, OOM, or cascading failures. Go's channels provide natural backpressure — the most important property to communicate in interviews.

// Pattern 1: bounded channel — natural backpressure
func pipeline(ctx context.Context, input <-chan Item) {
    output := make(chan ProcessedItem, 100) // backpressure kicks in at 100

    go func() {
        defer close(output)
        for item := range input {
            result := process(item)
            select {
            case output <- result: // blocks if consumer is slow
            case <-ctx.Done(): return
            }
        }
    }()

    for item := range output {
        writeToDatabase(item) // slow consumer — pressure propagates upstream
    }
}

// Pattern 2: rate limiter (golang.org/x/time/rate — token bucket)
limiter := rate.NewLimiter(rate.Limit(100), 10) // 100 req/s, burst 10

func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
    if !limiter.Allow() {
        http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests)
        return
    }
    // ... handle request
}

// Pattern 3: blocking acquire with context
if err := limiter.Wait(ctx); err != nil {
    return err // backpressure: context expired waiting for a token
}

// Pattern 4: non-blocking drop (best-effort)
select {
case queue <- item: // queue has space
default:            // queue full — drop or return 429
    log.Println("dropping item — queue full")
}
How does a bounded buffered channel provide natural backpressure in a pipeline?
What does rate.Limiter from golang.org/x/time/rate implement?
47. Implement a lock-free stack using atomic CAS operations and explain the ABA problem.

A lock-free data structure uses compare-and-swap (CAS) instead of mutexes — concurrent access without blocking. This is an advanced topic demonstrating deep understanding of memory ordering and Go's atomic package.

// Lock-free stack using atomic.Pointer (Go 1.19+)
type node[T any] struct {
    val  T
    next *node[T]
}

type LockFreeStack[T any] struct {
    head atomic.Pointer[node[T]]
}

func (s *LockFreeStack[T]) Push(val T) {
    n := &node[T]{val: val}
    for {
        old := s.head.Load()       // atomic read of current head
        n.next = old               // link new node to current head
        if s.head.CompareAndSwap(old, n) {
            return // success: head changed from old to n atomically
        }
        // CAS failed: another goroutine modified head; retry
    }
}

func (s *LockFreeStack[T]) Pop() (T, bool) {
    for {
        old := s.head.Load()
        if old == nil { var z T; return z, false }
        if s.head.CompareAndSwap(old, old.next) {
            return old.val, true
        }
    }
}

// ABA PROBLEM:
// Goroutine 1 reads head = A; pauses
// Goroutine 2: pops A, pushes B, pops B, pushes A again (same pointer!)
// Goroutine 1 resumes: CAS sees head == A → "unchanged" → succeeds
// But B has been removed — the stack is now corrupted

// WHY GO'S GC PREVENTS THE ABA PROBLEM:
// The GC does not reuse memory from a node until ALL pointers to it are gone
// If Goroutine 1 holds a reference to node A, A's memory is not recycled
// So when CAS sees the same pointer, it is guaranteed to be the same object
// → ABA is largely eliminated in GC-managed languages
What does atomic.Pointer.CompareAndSwap(old, new) guarantee?
Why does Go's garbage collector largely eliminate the ABA problem in lock-free structures?
48. Summarise: channel vs mutex decision guide, and the top concurrency pitfalls.

This summary covers the most tested concurrency patterns and pitfalls in Go technical interviews.

Channel vs Mutex — Decision Guide
ScenarioRecommended Tool
Passing data ownership between goroutinesChannel
Signalling an event / broadcastingChannel (close for broadcast)
Parallel pipeline of transformationsDirectional channels
Limiting concurrency (semaphore)Buffered channel or golang.org/x/sync/semaphore
Protecting a shared countersync/atomic or sync.Mutex
Protecting a shared map or structsync.Mutex or sync.RWMutex
One-time initialisationsync.Once
Wait for a group of goroutinessync.WaitGroup or errgroup
Read-heavy shared statesync.RWMutex or sync.Map
Request cancellation / deadlinescontext.Context
Top 10 Concurrency Pitfalls
PitfallSymptomFix
Goroutine leakNumGoroutine grows indefinitelyPass context; close channels; use goleak
Data raceInconsistent output; rare crashesRun -race; mutex/atomic/channels
Deadlock'all goroutines asleep'Consistent lock order; context timeouts
Nil interface error trapif err != nil passes on nil *TReturn bare nil, not (*T)(nil)
Loop variable captureAll goroutines print same valuePass as arg; use Go 1.22+ range
Mutex copied by valueBroken synchronisation silentlyAlways use *T containing mutex
time.After leak in loopGoroutine/timer accumulationUse NewTimer+Stop; prefer context
Send on closed channelpanic: send on closed channelOnly sender closes; done pattern
Lock held during I/OHigh latency under concurrencyRelease before I/O; re-acquire after
Missing defer wg.Done()wg.Wait() never returnsAlways defer wg.Done() in goroutine
// Five essential concurrent patterns

// 1. Worker with cancellation
go func() {
    for { select { case <-ctx.Done(): return; case job := <-jobs: process(job) } }
}()

// 2. One-time safe initialisation
var once sync.Once
once.Do(func() { /* runs exactly once across all goroutines */ })

// 3. Concurrent tasks with error collection
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { handle(err) }

// 4. Non-blocking channel operation
select { case ch <- val: ; default: /* channel full */ }

// 5. Timeout with automatic cleanup
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
Which tool is most appropriate for protecting a struct field read by 100 goroutines/sec and written once per minute?
What is the single most important flag to add to Go test runs for catching concurrency bugs early?
«
»

Comments & Discussions