Go Backend Developer Top 100 Interview Questions
100 Go backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the language core, goroutines and channels, context, the standard HTTP and database packages, testing, profiling, performance and system design, from the basics to production incidents.
Questions 21-40 of 100
Q21.How do you avoid unnecessary allocations in hot Go code?
Profile first, then remove allocations from the code paths that run most often. Fewer allocations mean less garbage collection work, lower processor use and steadier latency.
Techniques:
- Preallocate:
make([]T, 0, n)andmake(map[K]V, n)when the size is known. - Build strings efficiently:
strings.BuilderwithGrow, instead of repeated+in a loop. - Reuse buffers: pass buffers into functions, or use
sync.Poolfor short-lived objects created at high rates. - Avoid interface conversions in tight loops, since storing values in interfaces can allocate.
- Return values, not pointers, for small structs, so they can stay on the stack.
- Stream instead of buffering: encode JSON directly to the response writer instead of building a byte slice first.
How to measure:
go test -bench . -benchmemreports allocations per operation.- Heap profiles show which call sites allocate most.
testing.AllocsPerRuncounts allocations for a single function in a quick check.
Example
Repeated concatenation allocates on each step; a builder with a known size allocates once.
package main import ( "fmt" "strings" "testing" ) var sink string func joinNaive(parts []string) string { s := "" for _, p := range parts { s += p } return s } func joinBuilder(parts []string) string { n := 0 for _, p := range parts { n += len(p) } var b strings.Builder b.Grow(n) for _, p := range parts { b.WriteString(p) } return b.String() } func main() { parts := []string{"order", "-", "2026", "-", "000123", "-", "priority"} fmt.Println(testing.AllocsPerRun(100, func() { sink = joinNaive(parts) })) // 6 fmt.Println(testing.AllocsPerRun(100, func() { sink = joinBuilder(parts) })) // 1 }- Preallocate:
Q22.How does the `init` function work, and why should you use it sparingly?
Each package can have one or more
initfunctions, which run automatically after the package's variables are initialized and beforemainstarts. Packages initialize in dependency order, so a package's dependencies are always ready before its owninitruns.What it is used for:
- Registering implementations, such as database drivers or image formats, with a central registry.
- Computing package-level tables that cannot be written as simple variable initializers.
Why to use it sparingly:
- Hidden side effects: importing a package runs its
initcode, which can surprise readers and tests. - No error handling:
initcannot return errors, so failures must panic. - Hard to test: behavior depends on import order and global state.
- Slower startup if it does heavy work, such as network calls or reading large files.
Better alternatives: explicit constructors or setup functions called from
main, which make dependencies visible, return errors and can be configured in tests. Keepinitfor small, deterministic registration.Q23.What do the `make` and `new` built-in functions do?
Both allocate memory, but for different purposes.
newgives you a pointer to a zeroed value of any type.makecreates and initializes slices, maps and channels, which need internal structures before they can be used.new(T)make(T, args)Works for any type Works only for slices, maps and channels Returns a pointer, *T, to a zero valueReturns the initialized value itself, not a pointer new(int)gives a pointer to 0make([]int, 0, 10)gives an empty slice with capacity 10Practical notes:
newis rare in idiomatic Go; composite literals such as&Config{}are clearer and allow setting fields.- A map must be created with
makeor a literal before writing; the zero value is nil and panics on writes. make(chan T, n)sets a channel's buffer size; withoutn, the channel is unbuffered.- Use
make's capacity argument to avoid repeated reallocation when the final size is known.
Where a value lives, stack or heap, is decided by escape analysis, not by whether you used
newormake.Q24.How do constants and `iota` work in Go?
Constants are values fixed at compile time, such as numbers, strings and booleans. Untyped constants are precise and adapt to the type of the expression they are used in, so
const timeout = 5works as anint, afloat64or atime.Durationmultiplier.iotais a counter that starts at 0 in eachconstblock and increases by one on each line. Lines without an expression repeat the previous expression with the nextiotavalue, which makes enumerations and bit flags concise.Common patterns:
- Enumerations: a named type, such as
type Status int, with constants fromiota, plus aStringmethod for readable output. - Skipping zero: start with
_ = iotaso the zero value means "unset" rather than a real state. - Sizes and flags: expressions such as
1 << (10 * iota)or1 << iota.
Caution: constant values from
iotadepend on their order, so reordering or inserting lines changes stored or transmitted values. Assign explicit values when constants are persisted in databases or sent to other services.Example
An enumeration with a
Stringmethod, and size constants generated withiota.package main import "fmt" type Status int const ( Pending Status = iota Paid Shipped Cancelled ) func (s Status) String() string { return [...]string{"pending", "paid", "shipped", "cancelled"}[s] } const ( _ = iota KB = 1 << (10 * iota) MB GB ) func main() { fmt.Println(Paid, Shipped) // paid shipped fmt.Println(int(Cancelled)) // 3 fmt.Println(KB, MB, GB) // 1024 1048576 1073741824 fmt.Printf("%v %d\n", Status(1), Status(1)) // paid 1 }- Enumerations: a named type, such as
Q25.What does idiomatic Go code look like?
Idiomatic Go is simple, explicit and consistent: small packages, clear names, errors handled where they occur and abstractions added only when they pay for themselves.
Core habits:
- Format with
gofmt, so all code looks the same and style debates disappear. - Short, clear names: short names for short scopes, such as
iorr, and descriptive names for package-level identifiers. - Handle errors explicitly, adding context, and keep the normal path unindented by returning early on errors.
- Small interfaces defined by consumers, and functions that accept interfaces but return concrete types.
- Useful zero values, so types work without constructors where possible.
- Composition over deep type hierarchies.
- Pass
context.Contextas the first parameter of functions that do input and output or may block. - Document exported identifiers with comments that start with the identifier's name.
What to avoid:
- Getters named
GetX; useX(). - Packages named
utilorcommon. - Unnecessary goroutines, channels or generics where a plain function is clearer.
- Panics for ordinary errors.
Interview tip: quoting "clear is better than clever" and backing it with an example from your own code review experience shows you understand Go's culture, not just its syntax.
- Format with
Q26.What are goroutines, and how do they differ from operating system threads?
A goroutine is a function running concurrently, managed by the Go runtime rather than by the operating system. Goroutines are cheap enough that a server can run hundreds of thousands of them, typically one or more per connection or request.
Goroutine Operating system thread Starts with a small stack, a few kilobytes, that grows as needed Starts with a large fixed stack, often a megabyte or more Created and switched by the Go runtime, quickly and cheaply Created and switched by the kernel, which is slower Many goroutines share a small pool of threads Each thread is a separate kernel object Blocking calls park the goroutine, and the thread runs others Blocking calls block the whole thread What this means in practice:
- Writing simple, blocking-style code per request scales well, because blocked goroutines cost little.
- Goroutines still use memory and can leak; every goroutine needs a clear way to finish.
- Goroutines have no identity you can access or kill from outside; stop them by closing channels or cancelling a
context. - A panic in any goroutine crashes the whole program unless that goroutine recovers.
Q27.How does the Go scheduler run goroutines?
The runtime multiplexes many goroutines onto a few operating system threads. It uses three kinds of objects: goroutines, threads, and processors, which are logical slots that hold a queue of runnable goroutines. The number of processors is set by
GOMAXPROCS, by default the number of available cores.flowchart TD A["Global run queue"] --> B["Processor 1: local run queue"] A --> C["Processor 2: local run queue"] B --> D["Thread running a goroutine"] C --> E["Thread running a goroutine"] B -.->|"Work stealing"| C F["Network poller"] --> AHow it works:
- Each processor runs goroutines from its local queue on a thread.
- An idle processor steals work from other processors' queues, keeping all cores busy.
- A goroutine waiting on the network is parked, and the network poller makes it runnable again when data arrives, without holding a thread.
- A goroutine in a blocking system call releases its processor, so another thread can keep running other goroutines.
- Long-running goroutines are preempted, so one busy loop cannot starve the rest.
Why it matters: blocking-style code is efficient, and the runtime uses all cores without manual thread management.
Q28.How do unbuffered and buffered channels differ?
A channel passes values between goroutines. An unbuffered channel makes the sender and receiver meet: a send blocks until another goroutine receives. A buffered channel has a queue of fixed size: sends block only when the buffer is full, and receives block only when it is empty.
Unbuffered channel Buffered channel make(chan T)make(chan T, n)Send and receive happen together, a handoff Sender can run ahead by up to nvaluesGuarantees the receiver has the value when the send completes Decouples producer and consumer timing Best for synchronization and signaling Best for absorbing bursts and limiting concurrency Choosing a buffer size:
- Use unbuffered channels by default; they make timing explicit.
- Use a buffer of 1 for a result that must not block the sender if nobody is waiting, which prevents goroutine leaks.
- Larger buffers smooth bursts, but they hide backpressure problems if a consumer is permanently slower.
Other rules: receiving from a closed channel returns the zero value immediately; sending on a closed channel panics; operations on a nil channel block forever, which
selectcan use to disable a case.Example
An unbuffered handoff, a full buffer detected with
selectand draining a closed channel.package main import "fmt" func main() { unbuffered := make(chan string) go func() { unbuffered <- "handoff" }() fmt.Println(<-unbuffered) buffered := make(chan int, 2) buffered <- 1 buffered <- 2 fmt.Println(len(buffered), cap(buffered)) select { case buffered <- 3: fmt.Println("sent") default: fmt.Println("buffer full") } close(buffered) for v := range buffered { fmt.Println(v) } } // Output: // handoff // 2 2 // buffer full // 1 // 2Q29.How does the `select` statement work?
selectwaits on several channel operations at once and runs the first one that can proceed. If several are ready, it picks one at random, which prevents one channel from starving the others. Adefaultcase runs immediately when nothing is ready, making the operation non-blocking.Common patterns:
- Timeouts: a case on
time.After(d)or a context'sDonechannel limits how long to wait. - Cancellation: a case on
ctx.Done()in every loop that waits, so work stops when the request is cancelled. - Non-blocking send or receive: a
defaultcase, for example to drop a metric rather than block when a buffer is full. - Disabling a case: setting a channel variable to
nilmakes its case never ready, useful after a channel is closed.
Pitfalls:
time.Afterinside a loop creates a new timer each iteration; for long-running loops, reuse atime.Timeror use a context.- A goroutine sending its result on an unbuffered channel blocks forever if the
selectalready returned on a timeout; give the result channel a buffer of 1. - An empty
select {}blocks forever, which is occasionally used deliberately inmain.
Example
A fast call returns its result; a slow call loses to the timeout.
package main import ( "fmt" "time" ) func fetch(delay time.Duration) <-chan string { ch := make(chan string, 1) go func() { time.Sleep(delay) ch <- "result" }() return ch } func main() { for _, d := range []time.Duration{10 * time.Millisecond, 200 * time.Millisecond} { select { case r := <-fetch(d): fmt.Println(r) case <-time.After(50 * time.Millisecond): fmt.Println("timeout") } } } // Output: // result // timeoutThe result channel has a buffer of 1, so the slow goroutine can still send and exit after the timeout.
- Timeouts: a case on
Q30.How do you close channels correctly, and who should close them?
Close a channel only to tell receivers that no more values will come, and let the sender, the only party that knows when sending is finished, do the closing. Receivers never close channels they read from.
Rules:
- Only senders close: sending on a closed channel panics, so a receiver closing it can crash a sender.
- Close once: closing an already closed channel panics; with several senders, use a
sync.WaitGroupand close from one goroutine after all senders finish. - Closing is optional: channels are garbage collected like any value; close only when receivers need the signal, such as to end a
for rangeloop. - Detect closure:
v, ok := <-chreturnsok == falseonce the channel is closed and drained.
Patterns:
- Single producer:
defer close(out)at the top of the producing goroutine. - Many producers: a coordinator goroutine waits for all producers, then closes the channel.
- Broadcast a stop signal: closing a
donechannel wakes every goroutine waiting on it, though acontextis usually the clearer tool.
Q31.How do you wait for a group of goroutines with `sync.WaitGroup`?
A
WaitGroupcounts goroutines that are still running. Add to the counter before starting each goroutine, callDonewhen each one finishes and callWaitto block until the counter reaches zero. Recent Go versions also provide aGomethod that does the adding and finishing for you.Rules:
- Call
Addbefore starting the goroutine, not inside it, orWaitmay return before the goroutine has been counted. - Use
defer wg.Done()at the top of the goroutine, so it runs even if the function returns early. - Pass the WaitGroup by pointer, or capture it in a closure; copying it breaks the counter.
- Write results to separate slots, such as indexes of a preallocated slice, so goroutines do not race on shared data.
What it does not do: a WaitGroup does not collect errors or cancel other goroutines when one fails. For that, combine it with a channel of errors and a cancellable
context.Example
Each goroutine writes to its own index, so no lock is needed and the order is stable.
package main import ( "fmt" "sync" ) func main() { inputs := []int{2, 3, 4, 5} results := make([]int, len(inputs)) var wg sync.WaitGroup for i, n := range inputs { wg.Add(1) go func() { defer wg.Done() results[i] = n * n }() } wg.Wait() fmt.Println(results) // [4 9 16 25] }Since Go 1.22, each loop iteration has its own
iandn, so the closure captures the right values.- Call
Q32.How do mutexes and read-write mutexes protect shared state?
A
sync.Mutexallows only one goroutine at a time into a critical section, protecting shared data from concurrent modification. Async.RWMutexallows many readers at once, or a single writer, which helps when reads greatly outnumber writes.Good practices:
- Keep the lock next to the data it protects, usually as a field in the same struct, and document which fields it guards.
- Lock, then
defer Unlock, so every return path releases the lock. - Keep critical sections short: never perform network calls or slow input and output while holding a lock.
- Never copy a struct containing a mutex; use pointer receivers.
- Avoid nested locks where possible; when unavoidable, always acquire them in the same order to prevent deadlocks.
Choosing between them: an
RWMutexhas more overhead than a plain mutex, so it pays off only when reads are frequent and held for a while. Measure before switching.Alternatives: atomic operations for single counters or pointers, and channels when ownership of data should pass from one goroutine to another.
Example
A thousand goroutines update a shared map safely through a mutex.
package main import ( "fmt" "sync" ) type Counter struct { mu sync.Mutex counts map[string]int } func (c *Counter) Inc(key string) { c.mu.Lock() defer c.mu.Unlock() c.counts[key]++ } func main() { c := &Counter{counts: map[string]int{}} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc([]string{"paid", "failed"}[i%2]) }() } wg.Wait() fmt.Println(c.counts["paid"], c.counts["failed"]) // 500 500 }Removing the mutex makes the race detector report the problem, and the program can crash with a concurrent map write.
Q33.When should you use channels, and when should you use mutexes?
Use channels to pass ownership of data or to coordinate work between goroutines; use mutexes to protect shared state that several goroutines read and update in place. Both are idiomatic, and choosing the one that matches the problem keeps code simple.
Channels Mutexes Transfer data from one goroutine to another Guard data that stays in one place Pipelines, worker pools, events and signals Caches, counters, maps and connection state Make waiting, cancellation and backpressure explicit Make critical sections small and fast Can be slower and harder to follow for simple shared state Can deadlock if locks are nested carelessly Guidance:
- If you find yourself building a channel-based "lock" around a map, a mutex is simpler.
- If goroutines hand work to each other in stages, channels describe the flow better.
- For a single integer or pointer, consider the
sync/atomictypes.
The Go saying "share memory by communicating" is a guideline for coordination, not a rule against mutexes.
Q34.What is a data race, and how does the race detector find it?
A data race happens when two goroutines access the same memory at the same time, at least one access is a write and nothing orders them, such as a lock or a channel operation. The result is undefined: corrupted values, crashes or bugs that appear only under load.
Examples of races:
- Incrementing a shared counter from several goroutines without synchronization.
- Reading a map while another goroutine writes to it.
- Replacing a configuration struct while request handlers read its fields.
The race detector:
- Build or test with
-race, for examplego test -race ./.... - It instruments memory accesses at run time and reports any two unsynchronized accesses that actually happen during the run, with stack traces for both.
- It finds only races that the run exercises, so it needs tests that run code concurrently.
Practical use:
- Run tests with
-racein continuous integration. - Run a staging instance or load test with a race-enabled build occasionally, since the detector slows programs and uses more memory, which makes it unsuitable for normal production.
- Treat every report as a real bug, even if the code "seems to work".
Q35.How do you implement a worker pool in Go?
Start a fixed number of worker goroutines that read jobs from a shared channel and send results to another channel. The pool size limits concurrency, which protects databases and other services from too many simultaneous requests.
Structure:
- A jobs channel that the producer fills and then closes.
- N workers, each looping over the jobs channel with
for rangeuntil it is closed. - A results channel, closed by a coordinator goroutine after all workers finish, using a
WaitGroup. - The consumer reads results until the results channel closes.
Production details:
- Pass a
contextto workers, so they stop early when the request or the service is cancelled. - Decide how errors are reported: in the result struct, or by cancelling the rest of the work on the first failure.
- Size the pool from the bottleneck's capacity, such as the database connection pool size, not from the number of cores.
- Results arrive in completion order; include an ID or index if order matters.
Example
Three workers process six jobs; results arrive in any order, so the program checks the count and the total.
package main import ( "fmt" "sync" ) type job struct{ id, size int } type result struct{ id, cost int } func main() { jobs := make(chan job) results := make(chan result) var wg sync.WaitGroup for w := 0; w < 3; w++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { results <- result{id: j.id, cost: j.size * 10} } }() } go func() { for i := 1; i <= 6; i++ { jobs <- job{id: i, size: i} } close(jobs) }() go func() { wg.Wait() close(results) }() seen, total := map[int]bool{}, 0 for r := range results { seen[r.id] = true total += r.cost } fmt.Println(len(seen), total) // 6 210 }Q36.How do you build a fan-out and fan-in pipeline with channels?
A pipeline connects stages with channels: each stage receives values, processes them and sends results to the next stage. Fan-out starts several goroutines reading from the same channel to parallelize a slow stage; fan-in merges their outputs back into one channel.
flowchart LR A["Generate"] --> B["Square worker 1"] A --> C["Square worker 2"] A --> D["Square worker 3"] B --> E["Merge"] C --> E D --> E E --> F["Consumer"]Interview tip: interviewers often ask what happens if the consumer stops reading early; answering "every stage must watch a context, or its goroutines block forever" shows you have debugged real pipelines.
Rules that keep pipelines correct:
- Each stage closes its output channel when its input is exhausted, so downstream stages finish too.
- The merge stage uses a WaitGroup to close the merged channel after all inputs are drained.
- Every stage watches for cancellation, through a
context, so an early exit by the consumer does not leave goroutines blocked forever. - Order is not preserved after fan-out; carry an index if the consumer needs the original order.
Pipelines suit streaming work, such as reading records, transforming them and writing them out, while keeping memory bounded.
Example
Squares of 1 to 5 are computed by three workers and merged; the consumer sorts them because order is not guaranteed.
package main import ( "fmt" "slices" "sync" ) func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } func square(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out } func merge(inputs ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup for _, in := range inputs { wg.Add(1) go func() { defer wg.Done() for v := range in { out <- v } }() } go func() { wg.Wait() close(out) }() return out } func main() { source := generate(1, 2, 3, 4, 5) var got []int for v := range merge(square(source), square(source), square(source)) { got = append(got, v) } slices.Sort(got) fmt.Println(got) // [1 4 9 16 25] }Q37.How does `context` propagate cancellation and deadlines?
A
context.Contextcarries a cancellation signal, an optional deadline and request-scoped values across function calls and goroutines. Contexts form a tree: cancelling a parent cancels every context derived from it, so one cancelled request stops all the work it started.flowchart TD A["Request context, cancelled when the client disconnects"] --> B["WithTimeout: 2 s for the whole handler"] B --> C["WithTimeout: 300 ms for the database query"] B --> D["WithTimeout: 500 ms for the payment call"]How to use it:
- Accept
ctx context.Contextas the first parameter of functions that block or do input and output. - Derive child contexts with
WithTimeout,WithDeadlineorWithCancel, and always call the returnedcancelfunction, usually withdefer, to release resources. - Check
ctx.Done()in loops andselectstatements, and returnctx.Err()when it fires. - Pass contexts to standard library calls that accept them, such as
http.NewRequestWithContextand database methods ending inContext.
What not to do: store contexts in structs, pass
nilinstead ofcontext.Background(), or use context values for ordinary function parameters; values are for request-scoped data such as trace IDs.Example
A deadline stops a slow operation, and cancelling a parent cancels its child even though the child's own timeout is an hour.
package main import ( "context" "errors" "fmt" "time" ) func slowQuery(ctx context.Context, d time.Duration) (string, error) { select { case <-time.After(d): return "rows", nil case <-ctx.Done(): return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() fmt.Println(slowQuery(ctx, 10*time.Millisecond)) _, err := slowQuery(ctx, 200*time.Millisecond) fmt.Println(err, errors.Is(err, context.DeadlineExceeded)) parent, cancelParent := context.WithCancel(context.Background()) child, cancelChild := context.WithTimeout(parent, time.Hour) defer cancelChild() cancelParent() <-child.Done() fmt.Println(child.Err()) } // Output: // rows <nil> // context deadline exceeded true // context canceled- Accept
Q38.How do you avoid goroutine leaks?
A goroutine leaks when it blocks forever, usually on a channel send or receive that will never happen, or on a call with no timeout. Leaked goroutines accumulate memory and can hold connections or locks until the process restarts.
Common causes:
- Sending a result on an unbuffered channel after the receiver has given up because of a timeout.
- Reading from a channel that no one will ever close.
- Network or database calls without deadlines.
- Background loops that never check for shutdown.
How to prevent them:
- Give every goroutine an exit path: a
contextor a closed channel it watches. - Buffer single-result channels with capacity 1, so the sender can always finish.
- Set timeouts on all input and output.
- Own goroutines explicitly: the code that starts a goroutine is responsible for stopping it and waiting for it.
How to detect them: watch the goroutine count metric over time; a steady climb at constant traffic indicates a leak. Goroutine profiles show where blocked goroutines are waiting, and tests can compare
runtime.NumGoroutinebefore and after the code under test.Example
The unbuffered version leaves five goroutines blocked forever; the buffered version leaves none.
package main import ( "fmt" "runtime" "time" ) func compute() int { time.Sleep(30 * time.Millisecond) return 42 } func withTimeout(buffer int) int { ch := make(chan int, buffer) go func() { ch <- compute() }() select { case v := <-ch: return v case <-time.After(5 * time.Millisecond): return -1 } } func leaked(buffer int) int { before := runtime.NumGoroutine() for i := 0; i < 5; i++ { withTimeout(buffer) } time.Sleep(100 * time.Millisecond) return runtime.NumGoroutine() - before } func main() { fmt.Println("unbuffered leaked:", leaked(0)) // unbuffered leaked: 5 fmt.Println("buffered leaked:", leaked(1)) // buffered leaked: 0 }Q39.How do you run several goroutines and stop all of them when the first one fails?
Derive a cancellable context, start each task with it, send the first error to a buffered channel and cancel the context so the other tasks stop early. Wait for all goroutines before returning, so none are left running.
Steps:
ctx, cancel := context.WithCancel(parent)anddefer cancel().- A buffered error channel with room for every task, so no sender blocks.
- Each task runs in a goroutine; on error, it sends the error and calls
cancel. - A
WaitGroupwaits for all tasks, then the function returns the first error received, ornil.
Why each part matters:
- Tasks must respect
ctx.Done(), or cancellation has no effect. - Waiting for all goroutines guarantees no work continues after the function returns, which matters when tasks write shared results.
- Returning the first error, rather than all of them, matches most request handling; use
errors.Joinwhen callers need every failure.
The extended Go packages include a ready-made group type with this behavior, but understanding the pattern helps when you need variations, such as a concurrency limit.
Example
The inventory check fails quickly, and the slow pricing call stops instead of running for a full second.
package main import ( "context" "errors" "fmt" "sync" "time" ) func runAll(parent context.Context, tasks ...func(context.Context) error) error { ctx, cancel := context.WithCancel(parent) defer cancel() errs := make(chan error, len(tasks)) var wg sync.WaitGroup for _, task := range tasks { wg.Add(1) go func() { defer wg.Done() if err := task(ctx); err != nil { errs <- err cancel() } }() } wg.Wait() close(errs) return <-errs } func main() { start := time.Now() err := runAll(context.Background(), func(ctx context.Context) error { return errors.New("inventory unavailable") }, func(ctx context.Context) error { select { case <-time.After(time.Second): return nil case <-ctx.Done(): return nil } }, ) fmt.Println(err) // inventory unavailable fmt.Println(time.Since(start) < 500*time.Millisecond) // true }Q40.How do `sync.Once` and atomic operations work?
sync.Onceruns a function exactly once, however many goroutines call it at the same time; later callers wait until the first call has finished. Atomic operations, fromsync/atomic, read and update single values safely without a lock.Uses of
sync.Once:- Lazy initialization of expensive shared resources, such as a client or a compiled template.
- Closing something exactly once, such as a shutdown channel.
- The helpers
sync.OnceValueandsync.OnceFuncwrap common forms of this pattern.
Uses of atomics:
- Counters and gauges updated from many goroutines, with types such as
atomic.Int64. - Flags, such as "shutting down", with
atomic.Bool. - Swapping a pointer to immutable data, such as a configuration snapshot, with
atomic.Pointer[T].
Limits:
- Atomics protect one value at a time; updating two related values together still needs a mutex.
- If the function passed to
Oncepanics,Onceconsiders it done and will not retry; handle errors inside it or use a variant that stores the error. - Atomic code is subtle; prefer a mutex unless profiling shows lock contention.