7. Concurrency

Goroutines, channels, select, contexts, and the synchronization tools to write safe concurrent code.

Question: What is a goroutine, and how is it different from a traditional OS thread?

Answer: A goroutine is a lightweight thread of execution managed by the Go runtime. Goroutines are much cheaper than OS threads; they start with a small stack that can grow or shrink as needed, and their scheduling is handled by the Go runtime scheduler, not the OS kernel directly.

Explanation: Thousands or even millions of goroutines can run on a small number of OS threads. This makes it practical to launch a goroutine for every concurrent task (e.g., one per incoming network request). They are created with the go keyword followed by a function call.

Question: What is a channel in Go and how would you use one?

Answer: A channel is a typed conduit through which you can send and receive values with the <- operator. Channels enable communication and synchronization between goroutines.

Explanation: Channels are a core part of Go's concurrency model. They can be unbuffered (blocking until both sender and receiver are ready) or buffered (blocking on send only when the buffer is full). They are commonly used to coordinate work between goroutines, distribute units of work in a worker pool, or signal completion.

// Unbuffered channel
ch := make(chan int)

go func() {
    ch <- 42 // Send value to channel (blocks until received)
}()

val := <-ch // Receive value from channel (blocks until sent)

Question: What is the purpose of the select statement?

Answer: A select statement lets a goroutine wait on multiple communication operations. A select blocks until one of its cases can run, then it executes that case. It's chosen at random if multiple are ready.

Explanation: select is often used to implement timeouts or to handle messages from multiple channels. A default case can be added to make the select non-blocking. It is a powerful control structure for managing complex concurrent operations.

select {
case msg := <-ch1:
    fmt.Println("Received from ch1:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("Timeout: no message received after 1 second")
}

Question: What is the purpose of the context package?

Answer: The context package provides a way to carry request-scoped data, cancellation signals, and deadlines across API boundaries and between goroutines.

Explanation: It is essential for managing the lifecycle of a request in a concurrent system. For example, if a user cancels an HTTP request, the context can signal all downstream goroutines working on that request to stop their work and clean up, preventing wasted resources. It's standard practice for the first argument to functions in a call chain to be ctx context.Context.

Question: How do you wait for a set of goroutines to finish?

Answer: Use sync.WaitGroup to count goroutines and wait for completion.

Explanation: Increment before starting work, call Done when finished, and Wait in the caller.

var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    // work
}()
wg.Wait()

Question: When should you use sync.Mutex vs sync.RWMutex?

Answer: Use Mutex to guard shared state. Use RWMutex when reads greatly outnumber writes.

Explanation: RWMutex.RLock allows concurrent readers; writers still exclude all other access. Keep critical sections small.

var mu sync.RWMutex
mu.RLock()
v := data[key]
mu.RUnlock()
mu.Lock()
data[key] = v + 1
mu.Unlock()

Question: Who should close a channel and how do receivers detect closure?

Answer: The sender should close the channel. Receivers detect closure via the second boolean from receive.

Explanation: Only close when no more values will be sent. Do not close a channel from the receiver side or close a channel more than once.

close(ch)               // by sender
v, ok := <-ch           // ok == false when closed and drained

Question: What are channel directions and when to use them?

Answer: Use chan<- T for send-only and <-chan T for receive-only to document intent and enforce direction at compile time.

Explanation: This clarifies API ownership and prevents misuse.

Question: How do nil channels behave?

Answer: Sends and receives on a nil channel block forever; closing a nil channel panics. Use this to disable cases in select by setting channels to nil.

Question: How do you range over a channel and detect completion?

Answer: Use for v := range ch { ... } which stops when the channel is closed and drained. Only the sender should close the channel.

Question: Show a simple worker pool pattern.

Answer: Fan-out work to fixed workers, then close the results channel and wait.

jobs := make(chan int)
results := make(chan int)
var wg sync.WaitGroup
for w := 0; w < 4; w++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        for j := range jobs { results <- doWork(j) }
    }()
}
go func() { wg.Wait(); close(results) }()
for _, j := range input { jobs <- j }
close(jobs)
for r := range results { use(r) }

Question: How do you avoid goroutine leaks?

Answer: Always select on ctx.Done() or a done channel; ensure senders/receivers exit when peers stop; stop time.Ticker and drain timers.

Explanation: In loops, prefer time.NewTimer and timer.Reset over repeated time.After, which allocates.

Question: When should you use buffered vs unbuffered channels?

Answer: Use unbuffered channels for synchronous handoff; use buffered channels to decouple producers/consumers and smooth bursts.

Explanation: Size buffers based on expected concurrency and backpressure strategy.

Question: How do you make a goroutine responsive to cancellation?

Answer: Select on ctx.Done() alongside your work and return promptly when it fires.

for {
    select {
    case job := <-jobs:
        process(job)
    case <-ctx.Done():
        return
    }
}