Go Context Package 2026: Cancellation, Timeouts und Interview-Fragen

Umfassender Leitfaden zum Go context Package für Cancellation, Deadlines und request-scoped Values. Praktische Beispiele und häufige Interview-Fragen.

Go Context Package Cancellation und Timeouts Leitfaden

Das Go context Package stellt den Standardmechanismus bereit, um Deadlines, Cancellation-Signale und Request-spezifische Werte über API-Grenzen hinweg zu transportieren. Jede produktionsreife Go-Anwendung verwendet context: HTTP-Handler, Datenbank-Queries, gRPC-Aufrufe und Background-Worker sind alle darauf angewiesen für Graceful Shutdown und Timeout-Management.

Interview-Essentials

Context gehört zu den am häufigsten gefragten Themen in Go-Interviews. Interviewer erwarten von Kandidaten, dass sie Context-Propagation erklären, korrektes Cancellation-Handling demonstrieren und häufige Fallstricke wie das Speichern von Context in Structs vermeiden können.

Das Context Interface und seine vier Methoden

Das Context Interface definiert vier Methoden, die jede Context-Implementierung erfüllen muss. Das Verständnis dieser Methoden bildet die Grundlage für effektive Context-Nutzung.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() gibt einen Receive-Only-Channel zurück. Wenn dieser Channel geschlossen wird, erhält jede Goroutine, die darauf wartet, sofort das Signal. Das Pattern <-ctx.Done() blockiert bis zur Cancellation. Err() erklärt den Grund: entweder context.Canceled bei expliziter Cancellation oder context.DeadlineExceeded wenn ein Timeout oder eine Deadline überschritten wurde.

Contexts erstellen mit Background und TODO

Zwei Funktionen erstellen Root-Contexts: context.Background() und context.TODO(). Beide geben nicht-nil, leere Contexts zurück, die niemals gecanceled werden.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() dient als Parent für eingehende Requests, main-Funktionen und Initialisierungscode. TODO() fungiert als Platzhalter während des Refactorings, wenn die korrekte Context-Quelle noch nicht feststeht. Statische Analysetools können TODO()-Verwendungen für spätere Überprüfung markieren.

Cancellation mit WithCancel und WithCancelCause

Manuelle Cancellation ermöglicht es Parent-Goroutines, Child-Goroutines zu signalisieren, dass die Arbeit beendet werden soll. Dieses Pattern erscheint in Worker-Pools, Background-Tasks und Graceful-Shutdown-Implementierungen.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, eingeführt in Go 1.20, liefert reichhaltigere Fehlerinformationen als das einfache WithCancel. Die Funktion context.Cause() ruft den Fehler ab, der an die Cancel-Funktion übergeben wurde, was das Debugging in komplexen Systemen erleichtert.

Bereit für deine Go-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Timeouts und Deadlines für Request-Handling

HTTP-Handler, Datenbank-Aufrufe und externe API-Requests sollten immer Timeouts haben. Das context Package bietet WithTimeout und WithDeadline für diesen Zweck. Beide erstellen Contexts, die automatisch canceln, wenn die Zeit abläuft.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

Das defer cancel()-Pattern stellt sicher, dass Ressourcen freigegeben werden, auch wenn die Operation vor dem Timeout abgeschlossen wird. Das Auslassen dieses Aufrufs verursacht ein Ressourcenleck: Die Timer-Goroutine des Contexts bleibt aktiv, bis das Timeout abläuft.

Request-spezifische Werte mit WithValue

WithValue hängt request-spezifische Daten an einen Context an. Häufige Anwendungsfälle umfassen Request-IDs, Authentifizierungs-Tokens und Tracing-Spans. Die offizielle Dokumentation betont die Verwendung benutzerdefinierter Typen als Keys, um Kollisionen zu vermeiden.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Context-Werte bilden eine unveränderliche Kette: Jeder WithValue-Aufruf erstellt einen neuen Context, der den Parent umschließt. Lookups durchlaufen diese Kette, wodurch tief verschachtelte Werte langsamer zugänglich sind. Es sollten nur request-spezifische Daten gespeichert werden, keine Anwendungskonfiguration oder optionale Parameter.

WithoutCancel und AfterFunc Utilities

Go 1.21 fügte WithoutCancel hinzu für Operationen, die unabhängig von der Parent-Cancellation abgeschlossen werden müssen, wie Cleanup-Tasks. AfterFunc plant einen Callback, wenn Context-Cancellation auftritt.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel bewahrt die Werte des Parent-Contexts, ignoriert aber dessen Cancellation-Signal. Dieses Pattern eignet sich für Logging, Metrik-Emissionen und Datenbank-Commits, die unabhängig vom Request-Ergebnis erfolgreich sein müssen.

Interview-Fragen und Antworten

Die Vorbereitung auf ein technisches Go-Interview erfordert das Verständnis von Context-Interna und Best Practices. Diese Fragen tauchen häufig in Interviews bei Unternehmen auf, die Go für Backend-Services nutzen. Weitere Go-Interview-Vorbereitung findet sich im Context Package Interview-Fragen Modul.

Warum sollte context der erste Parameter sein?

Das Go-Team etablierte diese Konvention, um Context-Propagation sichtbar und konsistent zu machen. Context als ersten Parameter zu platzieren signalisiert Lesern, dass die Funktion Cancellation respektiert. Die Standardbibliothek folgt diesem Muster: http.Request.Context(), database/sql.QueryContext() und grpc.UnaryInterceptor erwarten alle context als erstes Argument.

Was passiert, wenn Context in einem Struct gespeichert wird?

Das Speichern von Context bricht das Request-Lifecycle-Modell. Ein Struct könnte den Request überleben, für den es erstellt wurde, was dazu führt, dass Operationen veraltete Cancellation-Signale verwenden oder neue Deadlines verpassen. Die Context-Dokumentation warnt explizit vor diesem Pattern.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

Wie behandelt man Context in Goroutines?

Goroutines müssen ctx.Done() in ihrer Hauptschleife oder Select-Anweisung prüfen. Das Ignorieren des Done-Channels erzeugt Goroutine-Leaks: Die Parent-Funktion kehrt zurück, aber die Goroutine verbraucht weiterhin Ressourcen.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

Wann sollte context.TODO() verwendet werden?

TODO() sollte während inkrementellem Refactoring verwendet werden, wenn Context-Unterstützung zu Legacy-Code hinzugefügt wird. Es markiert Stellen, die ordnungsgemäße Context-Propagation benötigen. Produktionscode sollte schließlich alle TODO()-Aufrufe durch echte Contexts von Request-Handlern oder Anwendungsinitialisierung ersetzen.

Häufige Fehler, die vermieden werden sollten

Context-Missbrauch führt zu subtilen Bugs, die sich unter Last oder während Shutdown-Sequenzen manifestieren. Das Erkennen dieser Patterns verhindert Produktionsvorfälle.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Wichtige Erkenntnisse für die Verwendung von Go Context in der Produktion

  • Context als ersten Parameter an Funktionen übergeben, niemals in Structs speichern
  • Immer cancel() nach WithTimeout, WithDeadline oder WithCancel mit defer aufrufen, um Ressourcenlecks zu verhindern
  • WithCancelCause und context.Cause() für besseres Error-Debugging in komplexen Cancellation-Ketten verwenden
  • <-ctx.Done() in Goroutine-Schleifen prüfen, um Graceful Shutdown zu ermöglichen und Leaks zu verhindern
  • Benutzerdefinierte Key-Typen für WithValue verwenden, um Kollisionen zwischen Packages zu vermeiden
  • WithoutCancel für Cleanup-Operationen anwenden, die unabhängig von der Parent-Cancellation abgeschlossen werden müssen
  • context.Background() für Anwendungsinitialisierung bevorzugen und context.TODO() nur während des Refactorings verwenden
Tägliche Challenge

Findest du den Bug in Go?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 29. August 2026

Tags

#go
#golang
#context
#concurrency
#interview

Teilen

Verwandte Artikel