Go Context Interview Questions: Cancellation, Deadlines, and Goroutine Leaks

Prepare for Go context interviews with deadline budgets, HTTP propagation, cancellable channel sends, worker-exit tests, and goroutine ownership boundaries.

Author: PracHub

Published: 9/15/2026

Go Context Interview Questions: Cancellation, Deadlines, and Goroutine Leaks

September 15, 2026

Quick Overview

Prepare for Go context interviews with deadline budgets, HTTP propagation, cancellable channel sends, worker-exit tests, and goroutine ownership boundaries.

Backend EngineerFree

Calling cancel() does not kill a goroutine. It signals that work should stop; the code doing that work must observe the signal and have a way to return. That distinction explains why a canceled HTTP request can leave a worker blocked on a channel, waiting for a dependency, or continuing an expensive calculation.

Go context interview questions become easier when you trace three separate events: cancellation is requested, a worker observes it, and the owner confirms the worker exited. This guide follows that sequence through deadlines, HTTP calls, database operations, and abandoned results.

Evidence boundary: API behavior comes from official Go documentation. The code and timing scenarios are original practice exercises, checked with local Go 1.27.1 where execution is stated. No candidate reports or claims about an employer’s question frequency are used.

Use PracHub’s Explain Go concurrency and database choices to practice the foundations, then explain the lifetime of each operation in the examples below.

Go Context cancellation branching from a request to HTTP and a worker, with a deadline clock

What does cancellation guarantee, and what does it leave to you?

Official contract: A CancelFunc requests abandonment without waiting for work to stop. Context methods can be used concurrently. The Done channel supplies the cancellation signal, while Err distinguishes cancellation from deadline expiry. These are the boundaries documented by the context package.

A function accepting ctx context.Context has not automatically become cancellable. Look inside it. Does its network call receive the context? Can its channel send stop waiting? Does its calculation check cancellation between bounded chunks? A context parameter that is never used is only a signature.

Original counterexample: A worker waits on release, a channel unrelated to its context. Canceling the context leaves that wait unchanged. The test must release the channel before the worker can exit. This demonstrates why cancellation and completion need separate signals.

Design ownership explicitly. The caller that starts a goroutine should know how it stops and how completion is observed. Depending on the design, completion can be a result channel, a WaitGroup, or a task group. An error returned to the HTTP client does not establish that all work started for that client has ended.

Cancellation is also not a rollback protocol. A downstream service may already have accepted a write before the caller stops waiting. If the result is ambiguous, reconcile using an operation identifier or idempotency contract; do not assume cancellation erased the side effect.

How do parent deadlines constrain child work?

The Go team’s context introduction explains the propagation model: derived contexts carry the parent’s lifetime into subordinate work. Canceling a child does not cancel its parent or an unrelated sibling. A child deadline cannot extend an earlier parent deadline.

Consider this synthetic request budget. The request starts at 0 ms with a deadline at 250 ms. At 90 ms, a helper asks for a 200 ms timeout. Its effective deadline remains 250 ms, leaving approximately 160 ms rather than a fresh 200 ms.

StepRemaining budget
Request begins250 ms
Helper starts at 90 ms160 ms
Retry considered at 210 ms40 ms

These values are arithmetic, not a service benchmark. They expose a common mistake: allocating an independent timeout at every layer and forgetting the end-to-end deadline. A retry requiring 80 ms cannot fit into the last row’s budget, even if an individual retry policy normally permits it.

A helper usually derives from the received context:

ctx, cancel := context.WithTimeout(parent, 200*time.Millisecond)
defer cancel()
return query(ctx)

Calling the returned cancel function also releases associated context resources when the operation finishes early. Deferring it inside a long-lived outer loop delays that cleanup until the whole function returns. Put one operation in a helper with its own defer, or cancel each iteration explicitly.

An expired context cannot be refreshed. Start a genuinely new operation with an appropriate owner and budget; do not replace the parent with Background() merely to make a timeout disappear.

Where does cancellation get lost across HTTP and database calls?

Official HTTP behavior: Incoming request contexts are canceled when the connection closes, an HTTP/2 request is canceled, or ServeHTTP returns. For an outgoing request, its context governs obtaining a connection, sending the request, and reading the response. See the net/http documentation.

That contract makes this boundary important:

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
    return err
}
resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

The request uses the caller’s context. In contrast, constructing unrelated work from context.Background() inside the handler breaks the cancellation chain. A timeout elsewhere in the client configuration does not restore the request’s original lifetime or remaining budget.

Official database guidance: The Go database cancellation guide demonstrates passing a derived context to QueryContext and releasing it afterward. Carry that context through the repository layer instead of dropping it at an interface boundary.

For a real driver, verify how it handles cancellation and what happens to the connection and server-side query. A method name alone does not establish that a remote transaction was rolled back. Close rows when finished, check iteration errors, and distinguish failing to acquire a connection from a query that started and then lost its response.

Original diagnostic trace: If a client disconnects but database work continues, record where the context was derived, which API received it, and when the driver returned. This separates a propagation bug from an operation that cannot promptly stop. Both can waste resources, but their repairs differ.

Why does an early consumer return strand a producer?

Suppose a forwarding worker receives a value and then sends it downstream:

v := <-in
out <- v

If the consumer abandons out, the send can wait indefinitely. Checking cancellation only before receiving from in does not help a goroutine already blocked on the second line.

The Go team’s pipelines guide develops cancellation for early consumer exit. Apply that idea at every blocking boundary, not only at the outermost loop. Here is an original forwarding exercise for integer channels:

func forward(ctx context.Context, in <-chan int, out chan<- int) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case v, ok := <-in:
            if !ok {
                return nil
            }
            select {
            case out <- v:
            case <-ctx.Done():
                return ctx.Err()
            }
        }
    }
}

The nested select is the important repair. It gives an abandoned send an exit path. The function intentionally does not close out: that responsibility belongs to whichever coordinator knows that every sender has finished. A receiver must not close a shared channel simply because it no longer wants results.

A buffer can make one blocked send disappear during a small test, but it does not establish a general lifetime policy. Once the buffer fills, the same wait returns. Capacity and cancellation answer different questions: how much work may queue, and when waiting should end.

Cancellation lifecycle from signaling through worker observation to owner-confirmed completion

Does select always prioritize cancellation?

No. Official language rule: When multiple cases can proceed, select chooses one through a uniform pseudo-random selection. Source order does not assign priority. The Go specification defines this behavior.

In the forwarding example, if both out <- v and <-ctx.Done() are ready, the send may still happen. The example provides a way out of a blocked send; it does not promise that cancellation is an atomic boundary after which no result can be delivered.

A preliminary ctx.Err() check can avoid some unnecessary work. It cannot eliminate a race in which cancellation arrives immediately after the check. If the requirement is “nothing may commit after this operation is invalidated,” enforce that rule at the commit boundary with synchronized state or an operation version.

Also examine expressions in a select case. Work used to compute a send value can run before the case is selected. Putting an expensive calculation directly in case out <- calculate(): does not make the calculation cancellable. Compute in bounded steps or call an API with a documented cancellation mechanism.

How should you cancel siblings and still wait for them?

For a request that requires several dependency results, define the failure policy before choosing a helper. Must every dependency succeed? Is one successful result enough? Can partial results be returned? These choices determine when sibling work becomes unnecessary.

Official errgroup behavior: errgroup.WithContext cancels its derived context on the first non-nil worker error or when Wait returns. Wait waits for all launched functions to return. The errgroup documentation also offers concurrency limits; it does not forcibly terminate a worker that ignores cancellation.

For an all-required request, pass the group context into each dependency, collect the error from Wait, and avoid using that derived context for new work after the wait. Bound concurrency before launching unbounded fan-out. A deadline limits lifetime, but it is not a substitute for a cap on simultaneous work.

Design inference: If one worker can block forever in an uncancellable dependency, waiting for the group can also block forever. Wrapping that call in another goroutine and timing out the wrapper only abandons the wait. Repair the dependency boundary, impose a supported I/O deadline, or give the work an explicitly managed longer lifetime.

When are context values or detached work appropriate?

Use context values for request-scoped information that must cross API boundaries, such as a trace identifier. Use explicit parameters for required business inputs and ordinary configuration. Sharing a context safely does not make a mutable map stored inside it safe for concurrent writes.

Official API detail: WithoutCancel, available since Go 1.21, retains access to parent values while removing cancellation and deadline behavior; its Done channel is nil. WithCancelCause and Cause can preserve a more specific reason while Err still reports the broad cancellation state. These details are in the context API reference.

Original design question: Should an audit task survive the HTTP handler’s return? If yes, give it a bounded queue, a service-level owner, a fresh explicit timeout, and shutdown handling. Detaching the request context alone supplies none of those controls. For work that must survive a process crash, use a durable handoff rather than an in-memory goroutine.

Choose deliberately which request information crosses that handoff. A full context can retain references longer than intended; copying the small identifiers the task needs often makes ownership clearer.

How can you test that cancellation actually works?

The critical assertion is worker completion, not merely a closed Done channel. In the local Go 1.27.1 exercise, a test hands a value to the forwarding worker through an unbuffered input. No receiver exists on the output. It then cancels and requires the worker’s result channel to report context.Canceled within a bounded test timeout.

The input handoff establishes that the worker received the value. The timeout is a failure bound, not synchronization. Separate tests cover natural input closure, an already-expired deadline, child cancellation leaving its parent alive, and a child timeout that cannot extend the parent deadline.

The official Go diagnostics guide distinguishes profiling and execution tracing. For a service investigation, inspect repeated goroutine stacks and cancellation-to-exit latency under realistic load. A growing count is a clue; identify the wait and its owner before calling it a leak.

A clean race-detector run checks a different failure class. It does not prove every goroutine exits. Keep cancellation tests focused on observable completion and ensure the tests themselves release any intentionally blocked worker.

Which PracHub questions are useful follow-ups?

The first two records are Go-focused. The remaining exercises transfer cancellation reasoning to crawler workers, pooled resources, and shutdown around durable writes. They are practice material, not a forecast of future questions.

Return to Explain Go concurrency and database choices and name the parent context, every blocking boundary, the cancellation owner, and the completion signal. Then change the requirement from “return on timeout” to “prove all owned workers exit” and explain what your design must add.

Sources and Further Reading


Comments (0)