Context and Cancellation
Go's context.Context propagates deadlines and cancellation across goroutine trees — replacing Java's CompletableFuture.cancel(), ExecutorService.shutdownNow(), and request-scoped thread-locals.
Introduction
In this lesson, you'll learn about context and cancellation in Go. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Java, you're familiar with go's context.context propagates deadlines and cancellation across goroutine trees — replacing java's completablefuture.cancel(), executorservice.shutdownnow(), and request-scoped thread-locals..
Go has its own approach to go's context.context propagates deadlines and cancellation across goroutine trees — replacing java's completablefuture.cancel(), executorservice.shutdownnow(), and request-scoped thread-locals., which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
// Functions accept ctx as FIRST parameter by convention
func fetchData(ctx context.Context, url string) ([]byte, error) {
// Create HTTP request bound to the context
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { return nil, err }
resp, err := http.DefaultClient.Do(req)
if err != nil {
// ctx.Err() tells you WHY: DeadlineExceeded or Canceled
return nil, fmt.Errorf("fetch failed: %w (ctx: %v)", err, ctx.Err())
}
defer resp.Body.Close()
// ... read body ...
return nil, nil
}
func processItems(ctx context.Context, items []int) error {
for _, item := range items {
// Check cancellation between items
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or DeadlineExceeded
default:
}
process(item)
}
return nil
}
func main() {
// Timeout: auto-cancelled after 5s
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // ALWAYS defer cancel to free resources
if err := processItems(ctx, []int{1, 2, 3}); err != nil {
fmt.Println("stopped:", err)
}
}Comparing to Java
Here's how you might have written similar code in Java:
import java.util.concurrent.*;
public class Cancellation {
CompletableFuture<String> fetchAsync(String url) {
return CompletableFuture.supplyAsync(() -> {
// No built-in way to check "am I cancelled?"
return doFetch(url);
});
}
void run() throws Exception {
var future = fetchAsync("https://api.example.com/data");
// Cancel after 2 seconds
ScheduledExecutorService sched = Executors.newSingleThreadScheduledExecutor();
sched.schedule(() -> future.cancel(true), 2, TimeUnit.SECONDS);
try {
String result = future.get(5, TimeUnit.SECONDS);
System.out.println(result);
} catch (CancellationException e) {
System.err.println("Cancelled");
} catch (TimeoutException e) {
System.err.println("Timed out");
}
}
}You may be used to different syntax or behavior.
context.Context is passed explicitly as the first function parameter — Java stores it in thread-locals
You may be used to different syntax or behavior.
context.WithTimeout/WithDeadline auto-cancel; context.WithCancel gives you a manual cancel() function
You may be used to different syntax or behavior.
Always defer cancel() immediately after WithTimeout/WithCancel — prevents context leak
You may be used to different syntax or behavior.
Check ctx.Done() channel in long loops; ctx.Err() reports reason (Canceled vs DeadlineExceeded)
You may be used to different syntax or behavior.
http.NewRequestWithContext binds a context to an HTTP request — the request is aborted when ctx is done
Step-by-Step Breakdown
1. Creating Contexts
Start with context.Background() at the top level. Wrap with WithTimeout for deadlines or WithCancel for manual cancellation.
var future = executor.submit(task);
future.get(5, TimeUnit.SECONDS);// Timeout:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Manual cancel:
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(2*time.Second); cancel() }()2. Pass ctx as First Argument
By convention, context is always the first parameter. This makes the cancellation chain explicit and visible in all function signatures.
CompletableFuture<String> fetchAsync(String url)// Convention: ctx is always first
func fetchUser(ctx context.Context, id int) (*User, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
// ...
}3. Checking Cancellation
In long-running loops, select on ctx.Done() to break early when cancelled or timed out.
if (Thread.currentThread().isInterrupted()) break;for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err() // Canceled or DeadlineExceeded
default:
}
process(item)
}4. Context Values (Request-Scoped Data)
context.WithValue attaches request-scoped data (user ID, trace ID) without passing through every function signature. Use typed keys to avoid collisions.
// Java: ThreadLocal.set(userId); ThreadLocal.get()type ctxKey string
const userIDKey ctxKey = "userID"
// In middleware:
ctx = context.WithValue(ctx, userIDKey, user.ID)
// In handler:
if uid, ok := ctx.Value(userIDKey).(int); ok {
fmt.Println("user:", uid)
}Common Mistakes
When coming from Java, developers often make these mistakes:
- context.Context is passed explicitly as the first function parameter — Java stores it in thread-locals
- context.WithTimeout/WithDeadline auto-cancel; context.WithCancel gives you a manual cancel() function
- Always defer cancel() immediately after WithTimeout/WithCancel — prevents context leak
Key Takeaways
- context.Background() → WithTimeout/WithCancel wraps it; always defer cancel() to free resources
- Pass ctx as the first parameter to every function that does I/O or long work
- select { case <-ctx.Done(): return ctx.Err() } breaks loops on cancellation
- http.NewRequestWithContext binds context to HTTP calls; context.WithValue for request-scoped metadata