Backend Development 8 min read

Using Go Context Cancellation: Patterns, Best Practices, and Common Pitfalls

This article explains why and how to use Go's context cancellation feature, covering listening for cancel events, emitting cancellations, time‑based timeouts, practical code examples, and important pitfalls to write faster and more robust backend services.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Using Go Context Cancellation: Patterns, Best Practices, and Common Pitfalls

This article explains how to leverage Go's context cancellation feature and presents patterns and best practices that make your programs faster and more robust.

Why cancellation? When a client aborts a request, downstream work such as database queries or HTTP calls can continue unnecessarily, wasting resources. Proper cancellation stops all downstream components as soon as the original request is known to be finished.

Listening for cancel events – a context provides a Done() channel that is closed when cancellation occurs. The pattern is to select on <-ctx.Done() alongside other work. Example:

func main() {
    http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        fmt.Fprint(os.Stdout, "processing request\n")
        select {
        case <-time.After(2 * time.Second):
            w.Write([]byte("request processed"))
        case <-ctx.Done():
            fmt.Fprint(os.Stderr, "request cancelled\n")
        }
    }))
}

Running the server and closing the browser before two seconds triggers the cancellation branch, printing “request cancelled”.

Submitting cancel events – use context.WithCancel to obtain a cancel function that can be called when an operation fails. Example with two dependent operations:

func operation1(ctx context.Context) error {
    time.Sleep(100 * time.Millisecond)
    return errors.New("failed")
}

func operation2(ctx context.Context) {
    select {
    case <-time.After(500 * time.Millisecond):
        fmt.Println("done")
    case <-ctx.Done():
        fmt.Println("halted operation2")
    }
}

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    go func() {
        if err := operation1(ctx); err != nil {
            cancel()
        }
    }()
    operation2(ctx)
}

If operation1 returns an error, cancel() stops operation2 promptly.

Time‑based cancellation – context.WithTimeout or context.WithDeadline automatically cancel after a duration or at a specific time. Example of an HTTP request with a 100 ms timeout:

func main() {
    ctx := context.Background()
    ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
    req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
    req = req.WithContext(ctx)
    client := &http.Client{}
    res, err := client.Do(req)
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    fmt.Println("Response received, status code:", res.StatusCode)
}

The request either succeeds within the deadline or fails with a “context deadline exceeded” error.

Traps and warnings – a context can be cancelled only once, so it should not be used to signal multiple errors. Pass the same context to every function that may need cancellation, and avoid wrapping an already cancellable context with another WithTimeout or WithCancel , which can lead to confusing behaviour.

BackendConcurrencyGoBest PracticestimeoutContextCancellation
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.