Skip to content

Expert · Careers and interviews · topic 44 of 46 · 45 min

Concurrency interview questions and coding rounds

Explanation

Almost every Go interview asks about concurrency: goroutines versus threads, buffered versus unbuffered channels, deadlocks, races, mutex versus channel, and context. Coding rounds often ask you to fetch many URLs with limited parallelism, build a worker pool, or write a rate limiter. Practise these patterns until you can write them without notes.

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)

go
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
}
Very common coding-round task. golang.org/x/sync/errgroup with SetLimit does the same and also collects the first error.

Pattern 2: worker pool with results

go
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
}
Every send also watches ctx.Done(), so cancelling the context never leaves goroutines stuck.

Pattern 3: a simple token-bucket rate limiter

go
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
}
Tokens refill at perSec up to burst; each request spends one. In production, golang.org/x/time/rate provides a tested limiter.

Questions to expect

QuestionKey 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).
Tip: In a live coding round, say out loud: who sends, who receives, who closes each channel, and how every goroutine ends. That single habit prevents most concurrency bugs and impresses interviewers.

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 interviews

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.

go
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
}
errgroup.WithContext(ctx) would also cancel the remaining calls after the first error; mention it as an improvement.

Practice (0/4 done)

Task 1 · Question

How do you stop a running goroutine from another goroutine?

Task 2 · Question

sem := make(chan struct{}, 3). What does it limit when every worker does sem <- struct{}{} before working and <-sem after?

Task 3 · Question

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?

Task 4 · Practice

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

Question 1

1.Why are goroutines cheaper than OS threads?

Question 2

2.Which package offers a tested token-bucket rate limiter?

Question 3

3.What does errgroup.Group.Wait return?

Question 4

4.A goroutine panics without recover. What happens?

Read more (free)

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.

From Channels: passing data between goroutines

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.

From Goroutines and WaitGroup

What is a deadlock?Related

Answer: Threads waiting for each other's locks forever

Circular waiting means none can proceed.

From Concurrency 1: threads, mutexes and data races

Which is best for a simple hit counter shared by many goroutines?Related

Answer: atomic.Int64

atomic.Int64 gives safe, fast Add and Load.

From Mutex, Once, atomic and the race detector

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

  1. 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.
  2. Module 10 exam: Careers and interviews · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. 46 topics left in the course before the final exam.

← Previous: Go language interview questions