Expert · Careers and interviews · topic 44 of 46 · 45 min
Concurrency interview questions and coding rounds
Explanation
Goroutine versus OS thread
A goroutine is managed by the Go runtime, not the operating system. It starts with a small stack of a few kilobytes that grows as needed, and the runtime scheduler multiplexes many goroutines onto a few OS threads (often described as the G-M-P model: goroutines, machine threads, processors). Creating and switching goroutines is much cheaper than threads, so a server can run hundreds of thousands. GOMAXPROCS controls how many threads run Go code at once.
Buffered versus unbuffered channels
An unbuffered channel synchronises: the send completes only when a receiver takes the value. A buffered channel of capacity n lets n sends complete without a receiver; sends block when it is full and receives block when it is empty. Use unbuffered for hand-offs and signals, buffered to absorb bursts or as a semaphore.
Deadlocks and goroutine leaks
A deadlock is when goroutines wait for each other forever. If ALL goroutines are blocked, the runtime stops with "all goroutines are asleep - deadlock!". If only some are stuck (for example a sender whose receiver timed out and left), the program keeps running but leaks those goroutines. Prevent leaks with buffered result channels, context cancellation, and clear ownership of who closes each channel.
Mutex or channel?
Channels are for passing ownership of data and for signalling between goroutines (pipelines, worker pools, done signals). A mutex is for protecting shared state that several goroutines read and update (a cache, a counter). Both are correct; choose the one that makes the code simplest. Always mention the race detector (go test -race) when discussing either.
Pattern 1: bounded parallelism (a semaphore)
func fetchAll(ctx context.Context, urls []string, limit int) []string {
results := make([]string, len(urls))
sem := make(chan struct{}, limit) // at most "limit" fetches at once
var wg sync.WaitGroup
for i, u := range urls {
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{} // acquire a slot
defer func() { <-sem }() // release it
results[i] = fetch(ctx, u)
}()
}
wg.Wait()
return results
}Pattern 2: worker pool with results
func process(ctx context.Context, jobs []int, workers int) []int {
in := make(chan int)
out := make(chan int)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for j := range in {
select {
case out <- j * j:
case <-ctx.Done():
return
}
}
}()
}
go func() {
defer close(in)
for _, j := range jobs {
select {
case in <- j:
case <-ctx.Done():
return
}
}
}()
go func() { wg.Wait(); close(out) }()
var res []int
for r := range out {
res = append(res, r)
}
return res // order not guaranteed
}Pattern 3: a simple token-bucket rate limiter
type Limiter struct {
mu sync.Mutex
tokens float64
max float64
perSec float64
last time.Time
}
func NewLimiter(perSec, burst float64) *Limiter {
return &Limiter{tokens: burst, max: burst, perSec: perSec, last: time.Now()}
}
func (l *Limiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
l.tokens = min(l.max, l.tokens+now.Sub(l.last).Seconds()*l.perSec)
l.last = now
if l.tokens >= 1 {
l.tokens--
return true
}
return false
}Questions to expect
| Question | Key points to mention |
|---|---|
| What happens when main returns? | The program exits; all goroutines stop. Use WaitGroup or channels to wait. |
| How do you stop a goroutine? | You cannot kill it from outside; it must return. Signal with ctx.Done() or a closed channel. |
| What does select do with several ready cases? | Picks one at random. |
| How do you find races? | go test -race / go run -race; fix with mutex, atomic or channels. |
| What is context for? | Deadlines and cancellation across API boundaries; ctx first parameter; always cancel. |
| Panic in a goroutine? | Crashes the whole program unless that goroutine recovers. |
| What is a data race vs a race condition? | Data race: unsynchronised access to memory. Race condition: wrong result due to timing, even with locks (for example check-then-act). |
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Concurrency interview questions and coding rounds · 1/7
Concurrency questions appear in almost every Go interview.
Prefer a video by a teacher? Find free videos on “golang concurrency interview questions worker pool semaphore errgroup” on YouTube ↗
In real life: The "check 100 PIN codes, at most 5 at a time" round
A logistics company asks: "We have 100 PIN codes. Call our serviceability API for each, at most 5 calls at a time, stop everything if the whole job takes more than 3 seconds, and return which PIN codes are serviceable." A strong answer combines a context with timeout, a semaphore or errgroup with SetLimit, a mutex-protected result map (or per-index slice), and a WaitGroup, then explains why it is race-free.
func serviceable(ctx context.Context, pins []string, check func(context.Context, string) (bool, error)) (map[string]bool, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
var (
mu sync.Mutex
out = make(map[string]bool)
g errgroup.Group // golang.org/x/sync/errgroup
)
g.SetLimit(5)
for _, pin := range pins {
g.Go(func() error {
ok, err := check(ctx, pin)
if err != nil {
return fmt.Errorf("pin %s: %w", pin, err)
}
mu.Lock()
out[pin] = ok
mu.Unlock()
return nil
})
}
err := g.Wait() // waits for all; returns the first error
return out, err
}Practice (0/4 done)
How do you stop a running goroutine from another goroutine?
sem := make(chan struct{}, 3). What does it limit when every worker does sem <- struct{}{} before working and <-sem after?
A check-then-act on a map is done under two separate lock/unlock pairs. Is there a data race? Is there a race condition?
Without notes, write in 20 minutes: a function that downloads a list of URLs with at most 4 in parallel, a 5-second overall timeout, returns results in the same order as the input, and returns the first error. Then explain aloud how every goroutine ends.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Your portfolio project: a production-style Go APICareers and interviews · same module
- Go careers in India and a 30-day revision planCareers and interviews · same module
- Concurrency 1: threads, mutexes and data racesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
- Mutex, Once, atomic and the race detectorConcurrency: goroutines, channels and context
- Interview questions: traits, errors, pointers and concurrencyRust Programming · Rust careers and interviews
- select: waiting on several channelsConcurrency: goroutines, channels and context
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Why are goroutines cheaper than OS threads?
Answer: Small growable stacks and scheduling in user space by the Go runtime
The runtime multiplexes many goroutines onto few threads.
Which package offers a tested token-bucket rate limiter?
Answer: golang.org/x/time/rate
rate.NewLimiter(r, burst) with Allow or Wait.
What does errgroup.Group.Wait return?
Answer: The first non-nil error returned by any Go function
It waits for all functions and returns the first error.
A goroutine panics without recover. What happens?
Answer: The whole program crashes
An unrecovered panic in any goroutine terminates the process.
What does a buffered channel of capacity 3 allow?Related
Answer: Up to 3 sends without a waiting receiver
Sends block only when the buffer is full.
What is the difference between concurrency and parallelism?Related
Answer: Concurrency is overlapping tasks; parallelism is running at the same instant on several cores
Go gives concurrency; the runtime provides parallelism on multi-core machines.
What is a deadlock?Related
Answer: Threads waiting for each other's locks forever
Circular waiting means none can proceed.
Which is best for a simple hit counter shared by many goroutines?Related
Answer: atomic.Int64
atomic.Int64 gives safe, fast Add and Load.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Careers and interviews” before its exam: Your portfolio project: a production-style Go API, Go careers in India and a 30-day revision plan.
- Module 10 exam: Careers and interviews · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
- 46 topics left in the course before the final exam.