Context in Go: Timeout, Cancel und Wertübergabe
1. Übersicht
Wenn du einen Go-Service schreibst, der mehrere Goroutinen verarbeitet, brauchst du eine einheitliche Möglichkeit, den Goroutinen zu sagen: „Stoppt, ich brauche das Ergebnis nicht mehr” oder „Läuft nur 2 Sekunden, dann stellt ein”. context.Context wurde genau dafür entwickelt.
Dieser Artikel behandelt drei Hauptaufgaben, die du mit Context erledigst: manuelles Canceln, Setzen von Timeout/Deadline und die Übergabe von Request-Werten durch mehrere Funktionsaufrufe. Nach dem Lesen kannst du HTTP-Handler, DB-Aufrufe und Funktionen schreiben, die Goroutinen zur richtigen Zeit stoppen und keine Goroutinen auslaufen lassen.
2. Was ist Context und warum brauchst du ihn
context.Context ist ein Interface, das den „Kontext” einer Verarbeitungskette beschreibt (normalerweise ein Request). Es enthält:
- Cancellation-Signal.
- Deadline/Timeout.
- Einige kleine Key-Value-Paare, die an den Request gebunden sind.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Done()gibt einen Channel zurück, der geschlossen wird, wenn der Context abgebrochen oder die Deadline überschritten wird.Err()gibt den Grund an:context.Canceledodercontext.DeadlineExceeded.Value()ruft einen Wert ab, der an den Context gebunden ist.
Hinweis: Context wird nicht verwendet, um beliebige optionale Parameter zu übergeben. Es sollte nur Steuersignale und Request-Werte wie Request-ID oder den aktuellen Benutzer enthalten — nicht Config oder wichtige Business-Objekte.
3. Manuelles Canceln mit WithCancel
Verwende dies, wenn du selbst entscheiden möchtest, wann du abbrechen möchtest, z.B. wenn eine von mehreren Goroutinen ein Ergebnis hat, dann brich die anderen ab.
func fetchFromMultipleSources(ctx context.Context) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // rufe cancel() immer auf, um Ressourcen freizugeben
resultCh := make(chan string, 1)
for _, src := range []string{"cache", "db", "api"} {
go func(source string) {
select {
case <-time.After(100 * time.Millisecond):
resultCh <- source
cancel() // Ergebnis vorhanden, brich andere Quellen ab
case <-ctx.Done():
return // abgebrochen, beende vorzeitig
}
}(src)
}
select {
case res := <-resultCh:
return res, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
context.WithCancel(ctx)erstellt einen Child-Context und eine cancel-Funktion, um ihn abzubrechen.defer cancel()ist zwingend erforderlich: Wenn du es nicht aufrufst, „hängt” der Child-Context bis der Parent abgebrochen wird, was zu einem Goroutine-Leak führt, das aufctx.Done()wartet.- Die Goroutine, die zuerst fertig ist, ruft
cancel()auf; die anderen Goroutinen erhalten das Signal überctx.Done()und stoppen.
Häufiger Fehler: defer cancel() vergessen. Wenn die Funktion vorzeitig zurückkehrt (z.B. durch einen Fehler) und den Context nicht abbricht, bleibt die Goroutine bestehen, bis sie auf indirektem Weg vom Garbage Collector entfernt wird — tatsächlich ein Leak.
4. Timeout und Deadline mit WithTimeout, WithDeadline
4.1. WithTimeout für relative Zeitlimits
func callPaymentService(ctx context.Context, orderID string) error {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://payment.local/orders/"+orderID, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // könnte context.DeadlineExceeded sein
}
defer resp.Body.Close()
return nil
}
context.WithTimeout(ctx, 2*time.Second)bricht den Context automatisch nach 2 Sekunden ab.http.NewRequestWithContextbindet den Context an den Request; wenn die Deadline überschritten wird, brichthttp.Clientdie laufende Anfrage automatisch ab.- Wenn über 2 Sekunden vergehen, gibt
resp, err := http.DefaultClient.Do(req)einen Fehler zurück, dercontext.DeadlineExceededenthält.
Fehler überprüfen:
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("Zahlungsservice Timeout")
}
return err
}
4.2. WithDeadline für absolute Zeitpunkte
func processBeforeMidnight(ctx context.Context) error {
deadline := time.Date(2026, 10, 6, 23, 59, 59, 0, time.Local)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
select {
case <-time.After(5 * time.Hour):
return errors.New("Verarbeitung abgeschlossen")
case <-ctx.Done():
return ctx.Err()
}
}
- Verwende
WithDeadline, wenn du einen spezifischen Zeitpunkt hast (z.B. Ende des Tages);WithTimeoutist dagegen eine relative Zeitdauer von jetzt an. - Im Grunde ist
WithTimeout(ctx, d)nur ein dünner Wrapper, derWithDeadline(ctx, time.Now().Add(d))aufruft.
5. Wertübergabe via Context mit WithValue
Verwende dies für Request-Daten, nicht für Business-Parameter.
type requestIDKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func RequestIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey{}).(string)
return id, ok
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
ctx := WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
if id, ok := RequestIDFromContext(r.Context()); ok {
log.Printf("[%s] Bestellung wird verarbeitet", id)
}
}
- Verwende
type requestIDKey struct{}als eindeutigen Schlüssel — vermeide direkte String-Schlüssel, da sie leicht mit Schlüsseln anderer Pakete kollidieren können. WithRequestIDundRequestIDFromContextkapseln das Setzen/Abrufen; andere Pakete müssen nicht wissen, wie der echte Schlüssel aussieht.r.WithContext(ctx)erstellt einen neuen Request mit dem Context, der die Werte trägt; dieser wird durch die Middleware-Kette weitergeleitet.
Hinweis: Verwende nicht
context.WithValue, um*sql.DB, Config oder etwas Erforderliches für die Funktionsweise zu übergeben. Diese sollten explizite Parameter oder Struct-Felder sein. Context-Werte eignen sich nur für optionale Daten wie Request-ID, Trace-ID oder den angemeldeten Benutzer.
6. context.TODO und context.Background
func main() {
ctx := context.Background() // Root-Context, wird nie abgebrochen
run(ctx)
}
func legacyFunctionBeingRefactored() {
ctx := context.TODO() // Platzhalter, wird später durch echten Context ersetzt
_ = ctx
}
context.Background()ist der Root-Context; verwende ihn inmain(), Tests oder beim Top-Level-Initialisieren.context.TODO()wird verwendet, wenn du gerade refaktorierst und nicht sicher bist, welcher Context übergeben werden sollte — funktional ähnlich wieBackground(), aber signalisiert Absicht für Leser und Linter.
7. Context-Propagation durch mehrere Funktionsaufrufe
Konvention: ctx context.Context ist immer der erste Parameter.
func (s *OrderService) CreateOrder(ctx context.Context, order Order) error {
if err := s.validate(ctx, order); err != nil {
return err
}
return s.repo.Save(ctx, order)
}
func (r *OrderRepository) Save(ctx context.Context, order Order) error {
_, err := r.db.ExecContext(ctx,
"INSERT INTO orders (id, total) VALUES (?, ?)", order.ID, order.Total)
return err
}
ExecContextnimmt den Context; wenn die HTTP-Request oben abgebrochen wird (Client schließt Verbindung), bricht der DB-Treiber die laufende Query ab — vermeidet unnötige Connection-Nutzung.- Regel: Jede Funktion, die I/O aufruft (HTTP, DB, gRPC), sollte einen Context empfangen und ihn an Context-unterstützende APIs weitergeben (mit *Context-Suffix).
8. Verhalten mit Tests überprüfen
func TestFetchFromMultipleSources_ReturnsFastestSource(t *testing.T) {
ctx := context.Background()
result, err := fetchFromMultipleSources(ctx)
require.NoError(t, err)
assert.Contains(t, []string{"cache", "db", "api"}, result)
}
func TestCallPaymentService_TimesOutWhenSlow(t *testing.T) {
slowServer := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
}))
defer slowServer.Close()
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, slowServer.URL, nil)
_, err := http.DefaultClient.Do(req)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
<!-- go.mod: Assertion-Bibliothek hinzufügen (nicht XML, nur Notiz) -->
go get github.com/stretchr/testify
- Der erste Test verwendet
context.Background(), da kein Timeout-Kontrolle in normalen Unit-Tests nötig ist. - Der zweite Test setzt absichtlich ein 500ms-Timeout kürzer als die Server-Verarbeitungszeit (3s), um zu bestätigen, dass der zurückgegebene Fehler tatsächlich
context.DeadlineExceededist. httptest.NewServererstellt einen Test-Server; kein komplexes Mocking erforderlich.
9. Häufige Fehler
- cancel() nicht aufrufen: Rufe immer
defer cancel()sofort nachWithCancel/WithTimeout/WithDeadlineauf, auch wenn der Context von selbst abläuft — Go vet wird dich warnen, wenn du es vergisst. - Context in Struct-Feldern speichern: Context sollte als Funktionsparameter übergeben werden, nicht als Struct-Feld gespeichert (außer in Spezialfällen wie
http.Request). Speichern im Feld führt leicht zu veralteten Contexts. - Context für erforderliche Parameter verwenden: Wenn eine Funktion nicht korrekt läuft, wenn dieser Wert fehlt, übergebe ihn als normalen Parameter, nicht versteckt im Context.
- Neuen Context statt Parent verwenden: Wenn du
context.Background()innerhalb einer Funktion aufrufst, diectxvom Aufrufer erhält, verlierst du die Möglichkeit, von oben abzubrechen/timeout zu setzen — leite immer vom erhaltenen Context ab:context.WithTimeout(ctx, ...), nichtcontext.WithTimeout(context.Background(), ...).
Wann Context nicht nötig ist: Bei reinen Funktionen (pure functions) ohne I/O oder Goroutinen brauchst du keinen Context als Parameter — das fügt nur Rauschen hinzu.
10. Fazit
Context löst drei wesentliche Aufgaben in Go Concurrency: Cancellation-Signale, Zeitlimitierung und das Weitergeben einiger Werte durch mehrere Funktionsaufrufe. Die sicherste Regel ist: Empfange ctx immer als ersten Parameter, rufe immer defer cancel() auf und verwende WithValue nur für tatsächlich optionale Request-Werte. Wenn du diese Prinzipien beherrschst, schreibst du Go-Services ohne Goroutine-Leaks, die richtig reagieren, wenn der Client Timeout hat oder die Anfrage abbricht.