Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

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.

100 questions

Questions 21-40 of 100

  1. 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:

    1. Preallocate: make([]T, 0, n) and make(map[K]V, n) when the size is known.
    2. Build strings efficiently: strings.Builder with Grow, instead of repeated + in a loop.
    3. Reuse buffers: pass buffers into functions, or use sync.Pool for short-lived objects created at high rates.
    4. Avoid interface conversions in tight loops, since storing values in interfaces can allocate.
    5. Return values, not pointers, for small structs, so they can stay on the stack.
    6. Stream instead of buffering: encode JSON directly to the response writer instead of building a byte slice first.

    How to measure:

    • go test -bench . -benchmem reports allocations per operation.
    • Heap profiles show which call sites allocate most.
    • testing.AllocsPerRun counts 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
    }
  2. Q22.How does the `init` function work, and why should you use it sparingly?

    Each package can have one or more init functions, which run automatically after the package's variables are initialized and before main starts. Packages initialize in dependency order, so a package's dependencies are always ready before its own init runs.

    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:

    1. Hidden side effects: importing a package runs its init code, which can surprise readers and tests.
    2. No error handling: init cannot return errors, so failures must panic.
    3. Hard to test: behavior depends on import order and global state.
    4. 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. Keep init for small, deterministic registration.

  3. Q23.What do the `make` and `new` built-in functions do?

    Both allocate memory, but for different purposes. new gives you a pointer to a zeroed value of any type. make creates and initializes slices, maps and channels, which need internal structures before they can be used.

    new(T)make(T, args)
    Works for any typeWorks 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 10

    Practical notes:

    1. new is rare in idiomatic Go; composite literals such as &Config{} are clearer and allow setting fields.
    2. A map must be created with make or a literal before writing; the zero value is nil and panics on writes.
    3. make(chan T, n) sets a channel's buffer size; without n, the channel is unbuffered.
    4. 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 new or make.

  4. 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 = 5 works as an int, a float64 or a time.Duration multiplier.

    iota is a counter that starts at 0 in each const block and increases by one on each line. Lines without an expression repeat the previous expression with the next iota value, which makes enumerations and bit flags concise.

    Common patterns:

    • Enumerations: a named type, such as type Status int, with constants from iota, plus a String method for readable output.
    • Skipping zero: start with _ = iota so the zero value means "unset" rather than a real state.
    • Sizes and flags: expressions such as 1 << (10 * iota) or 1 << iota.

    Caution: constant values from iota depend 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 String method, and size constants generated with iota.

    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
    }
  5. 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:

    1. Format with gofmt, so all code looks the same and style debates disappear.
    2. Short, clear names: short names for short scopes, such as i or r, and descriptive names for package-level identifiers.
    3. Handle errors explicitly, adding context, and keep the normal path unindented by returning early on errors.
    4. Small interfaces defined by consumers, and functions that accept interfaces but return concrete types.
    5. Useful zero values, so types work without constructors where possible.
    6. Composition over deep type hierarchies.
    7. Pass context.Context as the first parameter of functions that do input and output or may block.
    8. Document exported identifiers with comments that start with the identifier's name.

    What to avoid:

    • Getters named GetX; use X().
    • Packages named util or common.
    • 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.

  6. 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.

    GoroutineOperating system thread
    Starts with a small stack, a few kilobytes, that grows as neededStarts with a large fixed stack, often a megabyte or more
    Created and switched by the Go runtime, quickly and cheaplyCreated and switched by the kernel, which is slower
    Many goroutines share a small pool of threadsEach thread is a separate kernel object
    Blocking calls park the goroutine, and the thread runs othersBlocking 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.
  7. 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"] --> A

    How it works:

    1. Each processor runs goroutines from its local queue on a thread.
    2. An idle processor steals work from other processors' queues, keeping all cores busy.
    3. A goroutine waiting on the network is parked, and the network poller makes it runnable again when data arrives, without holding a thread.
    4. A goroutine in a blocking system call releases its processor, so another thread can keep running other goroutines.
    5. 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.

  8. 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 channelBuffered channel
    make(chan T)make(chan T, n)
    Send and receive happen together, a handoffSender can run ahead by up to n values
    Guarantees the receiver has the value when the send completesDecouples producer and consumer timing
    Best for synchronization and signalingBest 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 select can use to disable a case.

    Example

    An unbuffered handoff, a full buffer detected with select and 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
    // 2
  9. Q29.How does the `select` statement work?

    select waits 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. A default case runs immediately when nothing is ready, making the operation non-blocking.

    Common patterns:

    • Timeouts: a case on time.After(d) or a context's Done channel 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 default case, for example to drop a metric rather than block when a buffer is full.
    • Disabling a case: setting a channel variable to nil makes its case never ready, useful after a channel is closed.

    Pitfalls:

    1. time.After inside a loop creates a new timer each iteration; for long-running loops, reuse a time.Timer or use a context.
    2. A goroutine sending its result on an unbuffered channel blocks forever if the select already returned on a timeout; give the result channel a buffer of 1.
    3. An empty select {} blocks forever, which is occasionally used deliberately in main.

    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
    // timeout

    The result channel has a buffer of 1, so the slow goroutine can still send and exit after the timeout.

  10. 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:

    1. Only senders close: sending on a closed channel panics, so a receiver closing it can crash a sender.
    2. Close once: closing an already closed channel panics; with several senders, use a sync.WaitGroup and close from one goroutine after all senders finish.
    3. Closing is optional: channels are garbage collected like any value; close only when receivers need the signal, such as to end a for range loop.
    4. Detect closure: v, ok := <-ch returns ok == false once 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 done channel wakes every goroutine waiting on it, though a context is usually the clearer tool.
  11. Q31.How do you wait for a group of goroutines with `sync.WaitGroup`?

    A WaitGroup counts goroutines that are still running. Add to the counter before starting each goroutine, call Done when each one finishes and call Wait to block until the counter reaches zero. Recent Go versions also provide a Go method that does the adding and finishing for you.

    Rules:

    1. Call Add before starting the goroutine, not inside it, or Wait may return before the goroutine has been counted.
    2. Use defer wg.Done() at the top of the goroutine, so it runs even if the function returns early.
    3. Pass the WaitGroup by pointer, or capture it in a closure; copying it breaks the counter.
    4. 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 i and n, so the closure captures the right values.

  12. Q32.How do mutexes and read-write mutexes protect shared state?

    A sync.Mutex allows only one goroutine at a time into a critical section, protecting shared data from concurrent modification. A sync.RWMutex allows many readers at once, or a single writer, which helps when reads greatly outnumber writes.

    Good practices:

    1. Keep the lock next to the data it protects, usually as a field in the same struct, and document which fields it guards.
    2. Lock, then defer Unlock, so every return path releases the lock.
    3. Keep critical sections short: never perform network calls or slow input and output while holding a lock.
    4. Never copy a struct containing a mutex; use pointer receivers.
    5. Avoid nested locks where possible; when unavoidable, always acquire them in the same order to prevent deadlocks.

    Choosing between them: an RWMutex has 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.

  13. 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.

    ChannelsMutexes
    Transfer data from one goroutine to anotherGuard data that stays in one place
    Pipelines, worker pools, events and signalsCaches, counters, maps and connection state
    Make waiting, cancellation and backpressure explicitMake critical sections small and fast
    Can be slower and harder to follow for simple shared stateCan deadlock if locks are nested carelessly

    Guidance:

    1. If you find yourself building a channel-based "lock" around a map, a mutex is simpler.
    2. If goroutines hand work to each other in stages, channels describe the flow better.
    3. For a single integer or pointer, consider the sync/atomic types.

    The Go saying "share memory by communicating" is a guideline for coordination, not a rule against mutexes.

  14. 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:

    1. Build or test with -race, for example go test -race ./....
    2. It instruments memory accesses at run time and reports any two unsynchronized accesses that actually happen during the run, with stack traces for both.
    3. It finds only races that the run exercises, so it needs tests that run code concurrently.

    Practical use:

    • Run tests with -race in 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".
  15. 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:

    1. A jobs channel that the producer fills and then closes.
    2. N workers, each looping over the jobs channel with for range until it is closed.
    3. A results channel, closed by a coordinator goroutine after all workers finish, using a WaitGroup.
    4. The consumer reads results until the results channel closes.

    Production details:

    • Pass a context to 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
    }
  16. 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:

    1. Each stage closes its output channel when its input is exhausted, so downstream stages finish too.
    2. The merge stage uses a WaitGroup to close the merged channel after all inputs are drained.
    3. Every stage watches for cancellation, through a context, so an early exit by the consumer does not leave goroutines blocked forever.
    4. 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]
    }
  17. Q37.How does `context` propagate cancellation and deadlines?

    A context.Context carries 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:

    1. Accept ctx context.Context as the first parameter of functions that block or do input and output.
    2. Derive child contexts with WithTimeout, WithDeadline or WithCancel, and always call the returned cancel function, usually with defer, to release resources.
    3. Check ctx.Done() in loops and select statements, and return ctx.Err() when it fires.
    4. Pass contexts to standard library calls that accept them, such as http.NewRequestWithContext and database methods ending in Context.

    What not to do: store contexts in structs, pass nil instead of context.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
  18. 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:

    1. Give every goroutine an exit path: a context or a closed channel it watches.
    2. Buffer single-result channels with capacity 1, so the sender can always finish.
    3. Set timeouts on all input and output.
    4. 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.NumGoroutine before 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
    }
  19. 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:

    1. ctx, cancel := context.WithCancel(parent) and defer cancel().
    2. A buffered error channel with room for every task, so no sender blocks.
    3. Each task runs in a goroutine; on error, it sends the error and calls cancel.
    4. A WaitGroup waits for all tasks, then the function returns the first error received, or nil.

    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.Join when 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
    }
  20. Q40.How do `sync.Once` and atomic operations work?

    sync.Once runs a function exactly once, however many goroutines call it at the same time; later callers wait until the first call has finished. Atomic operations, from sync/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.OnceValue and sync.OnceFunc wrap 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:

    1. Atomics protect one value at a time; updating two related values together still needs a mutex.
    2. If the function passed to Once panics, Once considers it done and will not retry; handle errors inside it or use a variant that stores the error.
    3. Atomic code is subtle; prefer a mutex unless profiling shows lock contention.