1. Which of the following statements about map concurrency in Go is true?
Answer: Concurrent reads are safe, but concurrent writes without synchronization cause a fatal runtime crash.
Go maps are not safe for concurrent modification. The runtime deliberately detects concurrent map access and responds harshly: it crashes the program with a fatal error like fatal error: concurrent map writes. This is not a recoverable panic — recover won’t help; the process dies.
The precise situation:
- Concurrent reads — safe as long as nothing is writing.
- A read racing with a write, or two writes racing — fatal error.
Because the crash is fatal, the rules are simple to state: don’t write to a map from more than one goroutine, and don’t read while another goroutine writes, without synchronization.
The standard solutions: a sync.Mutex or sync.RWMutex guarding the map, or the purpose-built sync.Map for specific high-contention patterns (append-only, write-once-read-many). In modern Go, maps hold pointers internally, and the race is against the internal resizing/layout — which is exactly why the runtime refuses to let it happen silently.
The interview answer: reads are safe concurrently, but any concurrent write (or read-during-write) is a fatal, non-recoverable runtime error.
Answer:
Concurrent reads are safe, but concurrent writes without synchronization cause a fatal runtime crash.
Go maps are not safe for concurrent modification. The runtime deliberately detects concurrent map access and responds harshly: it crashes the program with a fatal error like fatal error: concurrent map writes. This is not a recoverable panic — recover won’t help; the process dies.
The precise situation:
- Concurrent reads — safe as long as nothing is writing.
- A read racing with a write, or two writes racing — fatal error.
Because the crash is fatal, the rules are simple to state: don’t write to a map from more than one goroutine, and don’t read while another goroutine writes, without synchronization.
The standard solutions: a sync.Mutex or sync.RWMutex guarding the map, or the purpose-built sync.Map for specific high-contention patterns (append-only, write-once-read-many). In modern Go, maps hold pointers internally, and the race is against the internal resizing/layout — which is exactly why the runtime refuses to let it happen silently.
The interview answer: reads are safe concurrently, but any concurrent write (or read-during-write) is a fatal, non-recoverable runtime error.
2. Which function from the sync package allows waiting for a collection of goroutines to finish executing?
Answer: sync.WaitGroup.
WaitGroup coordinates a set of goroutines: the main goroutine blocks until all workers complete. Its three methods form the whole API:
var wg sync.WaitGroup
wg.Add(2) // two goroutines to wait for
go func() { defer wg.Done(); work() }()
go func() { defer wg.Done(); work() }()
wg.Wait() // blocks until both Done() calls
Add(n)— declares how many goroutines to wait for (ideally before starting them).Done()— called when a goroutine finishes; it decrements the counter.Wait()— blocks until the counter reaches zero.
The counter must not go negative, and Wait must not be called while Add is still racing — the standard pattern is Add before go, defer Done() inside each goroutine.
The interview answer: sync.WaitGroup, with its Add / Done / Wait trio.
Answer:
sync.WaitGroup.
WaitGroup coordinates a set of goroutines: the main goroutine blocks until all workers complete. Its three methods form the whole API:
var wg sync.WaitGroup
wg.Add(2) // two goroutines to wait for
go func() { defer wg.Done(); work() }()
go func() { defer wg.Done(); work() }()
wg.Wait() // blocks until both Done() calls
Add(n)— declares how many goroutines to wait for (ideally before starting them).Done()— called when a goroutine finishes; it decrements the counter.Wait()— blocks until the counter reaches zero.
The counter must not go negative, and Wait must not be called while Add is still racing — the standard pattern is Add before go, defer Done() inside each goroutine.
The interview answer: sync.WaitGroup, with its Add / Done / Wait trio.
3. What happens if you try to lock a sync.Mutex that is already locked by the same goroutine?
Answer: The goroutine blocks forever on itself — a deadlock. Go’s mutexes are non-reentrant.
sync.Mutex does not track which goroutine holds it, and it does not allow re-entry. When a goroutine that already holds the lock calls Lock() again on the same mutex, it tries to acquire a lock that will only be released when it — the same goroutine — unlocks. Since it’s blocked waiting for itself, it can never proceed.
func f() {
mu.Lock()
defer mu.Unlock()
f() // deadlock — f re-enters, blocks on itself
}
This is a genuine deadlock, not an error. There’s no runtime “you already hold this lock” check — the goroutine just parks forever.
The contrast is with languages like Java where intrinsic locks are reentrant. Go made a deliberate design choice: non-reentrant mutexes are simpler and catch lock-ordering bugs early. If you genuinely need re-entrant locking, the idiom is to restructure — don’t re-lock, or use separate levels of locking.
The interview answer: locking an already-held sync.Mutex from the same goroutine blocks forever — a self-deadlock.
Answer:
The goroutine blocks forever on itself — a deadlock. Go’s mutexes are non-reentrant.
sync.Mutex does not track which goroutine holds it, and it does not allow re-entry. When a goroutine that already holds the lock calls Lock() again on the same mutex, it tries to acquire a lock that will only be released when it — the same goroutine — unlocks. Since it’s blocked waiting for itself, it can never proceed.
func f() {
mu.Lock()
defer mu.Unlock()
f() // deadlock — f re-enters, blocks on itself
}
This is a genuine deadlock, not an error. There’s no runtime “you already hold this lock” check — the goroutine just parks forever.
The contrast is with languages like Java where intrinsic locks are reentrant. Go made a deliberate design choice: non-reentrant mutexes are simpler and catch lock-ordering bugs early. If you genuinely need re-entrant locking, the idiom is to restructure — don’t re-lock, or use separate levels of locking.
The interview answer: locking an already-held sync.Mutex from the same goroutine blocks forever — a self-deadlock.
4. What keyword is used to prevent race conditions when executing code across multiple goroutines?
Answer: sync.Mutex (along with sync.RWMutex) — Go has no synchronized or volatile keywords; mutual exclusion is explicit via the sync package.
A data race is what happens when two goroutines access the same memory without synchronization and at least one writes. The direct fix is a mutex: only one goroutine holds the lock at a time, so the critical section it guards is safe.
var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()
The family:
sync.Mutex— simple mutual exclusion: one holder at a time.sync.RWMutex— allows many concurrent readers, exclusive writer. Better when reads dominate.
Go’s answer to volatile (which exists in some languages as a memory-ordering hint) is different: the sync/atomic package for lock-free atomic operations, and the go test -race detector to catch races during development.
The interview answer: sync.Mutex / sync.RWMutex — explicit locks, with sync/atomic as the lock-free alternative.
Answer:
sync.Mutex (along with sync.RWMutex) — Go has no synchronized or volatile keywords; mutual exclusion is explicit via the sync package.
A data race is what happens when two goroutines access the same memory without synchronization and at least one writes. The direct fix is a mutex: only one goroutine holds the lock at a time, so the critical section it guards is safe.
var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()
The family:
sync.Mutex— simple mutual exclusion: one holder at a time.sync.RWMutex— allows many concurrent readers, exclusive writer. Better when reads dominate.
Go’s answer to volatile (which exists in some languages as a memory-ordering hint) is different: the sync/atomic package for lock-free atomic operations, and the go test -race detector to catch races during development.
The interview answer: sync.Mutex / sync.RWMutex — explicit locks, with sync/atomic as the lock-free alternative.
5. Which package provides atomic primitives for lock-free concurrency operations?
Answer: sync/atomic.
The standard library package is sync/atomic. It provides lock-free primitives that map to hardware atomic instructions:
atomic.AddInt64(&x, 1)— atomic increment.atomic.LoadInt64/atomic.StoreInt64— atomic read/write.atomic.CompareAndSwapInt64(&x, old, new)— CAS.atomic.Value— atomic storage of any type.
These are the building blocks for lock-free algorithms and for safely sharing counters/flags across goroutines without a mutex. They trade the simplicity of a lock for finer-grained, non-blocking operations.
The interview answer: sync/atomic — low-level lock-free atomic memory operations.
Answer:
sync/atomic.
The standard library package is sync/atomic. It provides lock-free primitives that map to hardware atomic instructions:
atomic.AddInt64(&x, 1)— atomic increment.atomic.LoadInt64/atomic.StoreInt64— atomic read/write.atomic.CompareAndSwapInt64(&x, old, new)— CAS.atomic.Value— atomic storage of any type.
These are the building blocks for lock-free algorithms and for safely sharing counters/flags across goroutines without a mutex. They trade the simplicity of a lock for finer-grained, non-blocking operations.
The interview answer: sync/atomic — low-level lock-free atomic memory operations.
6. What is the role of GOMAXPROCS environment variable?
Answer: It controls the number of OS threads that can execute user-level Go code simultaneously — effectively the number of logical CPUs (Ps) available to the scheduler.
GOMAXPROCS sets how many parallel execution contexts the Go scheduler runs. By default it matches the machine’s logical CPU count, which is almost always right. You can read/change it at runtime via runtime.GOMAXPROCS(n).
What it does not control: per-goroutine stack size, GC heap thresholds, or network limits. It’s purely about CPU parallelism for user code. Raising it above the core count rarely helps and can hurt (more thread-switching); lowering it below the core count deliberately limits parallelism.
The interview answer: GOMAXPROCS sets the number of OS threads running user Go code concurrently (defaults to logical CPUs).
Answer:
It controls the number of OS threads that can execute user-level Go code simultaneously — effectively the number of logical CPUs (Ps) available to the scheduler.
GOMAXPROCS sets how many parallel execution contexts the Go scheduler runs. By default it matches the machine’s logical CPU count, which is almost always right. You can read/change it at runtime via runtime.GOMAXPROCS(n).
What it does not control: per-goroutine stack size, GC heap thresholds, or network limits. It’s purely about CPU parallelism for user code. Raising it above the core count rarely helps and can hurt (more thread-switching); lowering it below the core count deliberately limits parallelism.
The interview answer: GOMAXPROCS sets the number of OS threads running user Go code concurrently (defaults to logical CPUs).
7. How can you enable the built-in Go data race detector during tests or builds?
Answer: Pass the -race flag: go test -race, go run -race, or go build -race.
The race detector instruments your program at compile time and, at runtime, monitors all memory accesses for conflicting unsynchronized reads/writes across goroutines. When it detects a race, it reports the exact conflicting accesses with stack traces.
Usage patterns:
go test -race ./...— the most common: run the test suite under race detection.go run -race/go build -race— instrument a program directly.
The detector is built into the toolchain (no import needed) and adds modest runtime overhead, so it’s standard practice to run tests with -race in CI. The interview answer: the -race flag on go test, go run, or go build.
Answer:
Pass the -race flag: go test -race, go run -race, or go build -race.
The race detector instruments your program at compile time and, at runtime, monitors all memory accesses for conflicting unsynchronized reads/writes across goroutines. When it detects a race, it reports the exact conflicting accesses with stack traces.
Usage patterns:
go test -race ./...— the most common: run the test suite under race detection.go run -race/go build -race— instrument a program directly.
The detector is built into the toolchain (no import needed) and adds modest runtime overhead, so it’s standard practice to run tests with -race in CI. The interview answer: the -race flag on go test, go run, or go build.
8. What does runtime.Gosched() do when called inside a goroutine?
Answer: It yields the processor — the current goroutine voluntarily gives up the CPU and is placed back in the runnable queue, letting other goroutines run.
runtime.Gosched() is a cooperative yield: “I don’t need to run right now; let someone else go.” The calling goroutine is moved to the back of the runnable queue, and the scheduler picks another goroutine. When its turn comes, it resumes.
Two clarifications:
- It does not pause or sleep — the goroutine stays runnable, just queued.
- It does not terminate the goroutine or trigger GC.
Its uses are niche — mostly in tight CPU loops or hand-rolled cooperative scheduling where you want to give other goroutines a chance without blocking. In practice, idiomatic Go rarely needs it, since channel/mutex operations already release the processor. The interview answer: Gosched voluntarily yields the CPU, letting other goroutines run, with the caller queued to resume later.
Answer:
It yields the processor — the current goroutine voluntarily gives up the CPU and is placed back in the runnable queue, letting other goroutines run.
runtime.Gosched() is a cooperative yield: “I don’t need to run right now; let someone else go.” The calling goroutine is moved to the back of the runnable queue, and the scheduler picks another goroutine. When its turn comes, it resumes.
Two clarifications:
- It does not pause or sleep — the goroutine stays runnable, just queued.
- It does not terminate the goroutine or trigger GC.
Its uses are niche — mostly in tight CPU loops or hand-rolled cooperative scheduling where you want to give other goroutines a chance without blocking. In practice, idiomatic Go rarely needs it, since channel/mutex operations already release the processor. The interview answer: Gosched voluntarily yields the CPU, letting other goroutines run, with the caller queued to resume later.
9. What is the behavior of sync.Once?
Answer: sync.Once guarantees a function passed to Do() runs exactly once, even across all goroutines and repeated calls.
var once sync.Once
once.Do(initSomething) // runs initSomething
once.Do(initSomething) // no-op — already done
The guarantees:
- No matter how many goroutines call
Do, the function runs exactly once — concurrent callers block until the first completes, then all see it done. - Subsequent
Docalls do nothing.
This makes it the standard tool for lazy singleton initialization — a resource (DB connection, config, logger) that should be created at most once, on first use, safely across goroutines. The interview answer: Do(f) runs f exactly once program-wide; every other call is a no-op.
Answer:
sync.Once guarantees a function passed to Do() runs exactly once, even across all goroutines and repeated calls.
var once sync.Once
once.Do(initSomething) // runs initSomething
once.Do(initSomething) // no-op — already done
The guarantees:
- No matter how many goroutines call
Do, the function runs exactly once — concurrent callers block until the first completes, then all see it done. - Subsequent
Docalls do nothing.
This makes it the standard tool for lazy singleton initialization — a resource (DB connection, config, logger) that should be created at most once, on first use, safely across goroutines. The interview answer: Do(f) runs f exactly once program-wide; every other call is a no-op.
10. What happens if you call WaitGroup.Add(-1) when the WaitGroup counter is already 0?
Answer: A runtime panic: panic: negative WaitGroup counter.
The WaitGroup counter must never go below zero. Done() is documented as equivalent to Add(-1), and both must be balanced so the counter stays non-negative. If an Add(-1) (or an unbalanced Done) would drive the counter negative, the runtime panics immediately.
This catches bugs like calling Done() more times than Add() was called — a sign of a misbehaving goroutine. The interview answer: it panics with negative WaitGroup counter.
Answer:
A runtime panic: panic: negative WaitGroup counter.
The WaitGroup counter must never go below zero. Done() is documented as equivalent to Add(-1), and both must be balanced so the counter stays non-negative. If an Add(-1) (or an unbalanced Done) would drive the counter negative, the runtime panics immediately.
This catches bugs like calling Done() more times than Add() was called — a sign of a misbehaving goroutine. The interview answer: it panics with negative WaitGroup counter.
Premium Content
Unlock sync & Concurrency and all premium lessons with a subscription.
From ₹199.99/year — See plans