Go context-tidsgränser förhindrar att långsamma DB-anrop och externa förfrågningar staplas upp. Lär dig hur du för vidare deadlines, avbryter jobb och väljer säkra standarder.

En enskild långsam förfrågan är sällan bara "långsam." Medan den väntar håller den en goroutine vid liv, binder minne för buffrar och svarsobjekt och upptar ofta en databasanslutning eller en plats i en pool. När tillräckligt många långsamma förfrågningar hopar sig slutar ditt API göra något användbart eftersom dess begränsade resurser sitter fast och väntar.
Du känner det vanligtvis på tre ställen. Goroutines ackumuleras och schemaläggningsöverhead ökar, så latensen blir sämre för alla. Databaspooler tar slut på fria anslutningar, så även snabba frågor börjar köa efter de långsamma. Minne stiger från pågående data och delvis byggda svar, vilket ökar GC-arbetet.
Att lägga till fler servrar löser ofta inte problemet. Om varje instans träffar samma flaskhals (en liten DB-pool, en långsam upstream, delade rate limits) flyttar du bara kön runt och betalar mer medan felen fortfarande skjuter i höjden.
Föreställ dig en handler som sprider arbetet: den läser en användare från PostgreSQL, anropar en betalningstjänst och sedan en rekommendationstjänst. Om rekommendationsanropet hänger sig och inget avbryter det, blir förfrågan aldrig klar. DB-anslutningen kan återlämnas, men goroutinen och HTTP-klientresurserna förblir bundna. Multiplicera det med hundratals förfrågningar så får du en långsam nerkylning.
Målet är enkelt: sätt en tydlig tidsgräns, sluta arbeta när tiden är ute, frigör resurser och returnera ett förutsägbart fel. Go context-tidsgränser ger varje steg en deadline så att arbete stoppas när användaren inte längre väntar.
En context.Context är ett litet objekt som du skickar ner i anropskedjan så att varje lager håller med om en sak: när den här förfrågan måste sluta. Tidsgränser är det vanliga sättet att förhindra att en långsam beroende binder din server.
En context kan bära tre sorters information: en deadline (när arbetet måste sluta), en avbrottssignal (någon bestämde sig för att sluta tidigare) och några request-avgränsade värden (använd sparsamt och aldrig för stora data).
Avbrott är ingen magi. En context exponerar en Done()-kanal. När den stängs är förfrågan avbruten eller dess tid är ute. Kod som respekterar context kontrollerar Done() (ofta med en select) och returnerar tidigt. Du kan också kolla ctx.Err() för att få reda på varför den slutade, vanligtvis context.Canceled eller context.DeadlineExceeded.
Använd context.WithTimeout för "stoppa efter X sekunder." Använd context.WithDeadline när du redan vet den exakta cut-off-tiden. Använd context.WithCancel när en förälder-villkor ska stoppa arbetet (klienten kopplade ner, användaren navigerade bort, du har redan svaret).
När en context avbryts är korrekt beteende tråkigt men viktigt: sluta göra arbete, sluta vänta på långsam I/O och returnera ett tydligt fel. Om en handler väntar på en databasfråga och contexten tar slut, returnera snabbt och låt databas-anropet avbrytas om det stöder context.
Den säkraste platsen att stoppa långsamma förfrågningar är gränsen där trafiken kommer in i din tjänst. Om en förfrågan ska time-outa vill du att det sker förutsägbart och tidigt, inte efter att den har bundit goroutines, DB-anslutningar och minne.
Börja vid kanten (load balancer, API-gateway, reverse proxy) och sätt ett hårt tak för hur länge en förfrågan får leva. Det skyddar din Go-tjänst även om en handler glömmer att sätta en tidsgräns.
Inuti din Go-server, konfigurera HTTP-tidsgränser så att servern inte väntar för alltid på en långsam klient eller ett stillastående svar. Minst bör du konfigurera timeouter för att läsa headers, läsa full request-body, skriva svaret och hålla idle-anslutningar levande.
Välj en standardbudget som matchar din produkt. För många API:er är 1 till 3 sekunder en rimlig utgångspunkt för typiska förfrågningar, med en högre gräns för kända långsamma operationer som exporter. Det exakta talet är mindre viktigt än att vara konsekvent, mäta det och ha en tydlig regel för undantag.
Streaming-svar behöver extra omsorg. Det är lätt att oavsiktligt skapa en oändlig ström där servern håller anslutningen öppen och skriver små bitar för evigt, eller väntar för alltid innan första bytet skickas. Bestäm i förväg om en endpoint verkligen är en stream. Om den inte är det, hantera en maximal total tid och maximal time-to-first-byte.
När gränsen väl har en tydlig deadline är det mycket enklare att föra vidare den genom hela förfrågan.
Den enklaste platsen att börja är HTTP-handlern. Det är där en förfrågan går in i ditt system, så det är en naturlig plats att sätta ett hårt tak.
Skapa en ny context med en deadline, och se till att du avbryter den. Skicka sedan den contexten till allt som kan blockera: databasarbete, HTTP-anrop eller långsamma beräkningar.
func (s *Server) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
userID := r.URL.Query().Get("id")
if userID == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.loadUser(ctx, userID)
if err != nil {
writeError(w, ctx, err)
return
}
writeJSON(w, http.StatusOK, user)
}
ctx in i varje blockerande anropEn bra regel: om en funktion kan vänta på I/O bör den acceptera en context.Context. Håll handlers läsbara genom att flytta detaljer till små hjälpfunktioner som loadUser.
func (s *Server) loadUser(ctx context.Context, id string) (User, error) {
return s.repo.GetUser(ctx, id) // repo should use QueryRowContext/ExecContext
}
Om deadlinen nås (eller klienten kopplar ner), stoppa arbetet och returnera ett användarvänligt svar. En vanlig mappning är context.DeadlineExceeded till 504 Gateway Timeout, och context.Canceled till "client is gone" (ofta utan svarskropp).
func writeError(w http.ResponseWriter, ctx context.Context, err error) {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
// Client went away. Avoid doing more work.
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
}
Detta mönster förhindrar ansamlingar. När timern går ut får varje context-medveten funktion i kedjan samma stoppsignal och kan avsluta snabbt.
När din handler har en context med en deadline är den viktigaste regeln enkel: använd samma ctx hela vägen in i databas-anropet. Det är så tidsgränser stoppar arbete istället för att bara hindra din handler från att vänta.
Med database/sql, föredra de context-medvetna metoderna:
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
row := s.db.QueryRowContext(ctx,
"SELECT id, email FROM users WHERE id = $1",
r.URL.Query().Get("id"),
)
var id int64
var email string
if err := row.Scan(&id, &email); err != nil {
// handle below
}
}
Om handlerns budget är 2 sekunder bör databasen få bara en del av den. Lämna tid för JSON-kodning, andra beroenden och felhantering. En enkel startpunkt är att ge Postgres 30% till 60% av totalbudgeten. Med en 2-sekunders handler-deadline kan det vara 800ms till 1.2s.
När contexten avbryts ber drivrutinen Postgres att stoppa frågan. Vanligtvis returnerar anslutningen till poolen och kan återanvändas. Om avbrottet sker under ett nätverksproblem kan drivrutinen kasta den anslutningen och öppna en ny senare. Hur som helst undviker du en goroutine som väntar för evigt.
När du kontrollerar fel, behandla timeouter annorlunda än verkliga DB-fel. Om errors.Is(err, context.DeadlineExceeded) betyder det att du tog slut på tid och bör returnera en timeout. Om errors.Is(err, context.Canceled) betyder det att klienten försvann och du bör sluta tyst. Andra fel är vanliga query-problem (fel SQL, saknad rad, behörigheter).
Om din handler har en deadline bör även dina utgående HTTP-anrop respektera den. Annars ger klienten upp men din server fortsätter vänta på en långsam upstream och binder goroutines, sockets och minne.
Bygg utgående requests med förälderns context så färdas avbrottet automatiskt:
func fetchUser(ctx context.Context, url string) ([]byte, error) {
// Add a small per-call cap, but never exceed the parent deadline.
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() // always close, even on non-200
return io.ReadAll(resp.Body)
}
Denna per-anrop-tidsgräns är ett skyddsnät. Förälderns request-deadline är fortfarande den verkliga bossen. En klocka för hela förfrågan, plus mindre lock för riskfyllda steg.
Ställ också in timeouter på transportnivå. Context avbryter requesten, men transport-timeouter skyddar dig från långsamma handskakningar och servrar som aldrig skickar headers.
En detalj som ställer till problem: respons-body måste stängas på alla vägar. Om du returnerar tidigt (statuskontroll, JSON-dekodfel, context-timeout), stäng ändå body. Att läcka bodies kan tyst tömma anslutningar i poolen och leda till "slumpmässiga" latensspikar.
Ett konkret scenario: ditt API anropar en betalningsleverantör. Klienten time-outar efter 2 sekunder, men upstream hänger i 30 sekunder. Utan request-avbrott och transport-timeouter betalar du för den 30-sekunders väntan för varje övergiven förfrågan.
En enskild förfrågan rör sig ofta över mer än en långsam sak: handlerarbete, en databasfråga och en eller flera externa API:er. Om du ger varje steg en generös tidsgräns växer total tiden tyst tills användarna märker det och din server börjar stapla väntande jobb.
Budgetering är den enklaste fixen. Sätt en förälders-deadline för hela förfrågan, och ge sedan varje beroende en mindre bit. Barn-deadlines bör vara tidigare än föräldern så att du misslyckas snabbt och fortfarande har tid att returnera ett rent fel.
Tumregler som håller i verkliga tjänster:
Undvik att stapla tidsgränser som slåss mot varandra. Om din handler-context har en 2-sekunders deadline och din HTTP-klient har en 10-sekunders timeout är du säker men förvirrande. Om det är tvärtom kan klienten kapa tidigt av orelaterade skäl.
För bakgrundsarbete (audit logs, metrics, e-post) återanvänd inte request-contexten. Använd en separat context med egen kort timeout så att klientavbrott inte dödar viktig städning.
De flesta timeout-buggar är inte i handlern. De händer ett eller två lager ner, där deadlinen tyst försvinner. Om du sätter timeouter vid kanten men ignorerar dem i mitten kan du fortfarande få goroutines, DB-frågor eller HTTP-anrop som fortsätter efter att klienten gått.
Mönstren som oftast visar sig är enkla:
context.Background() (eller TODO). Det kopplar bort arbetet från klientens avbrott och handler-deadline.ctx.Done(). Förfrågan är avbruten men din kod fortsätter vänta.context.WithTimeout. Du får många timrar och förvirrande deadlines.ctx på blockerande anrop (DB-frågor, utgående HTTP, message publishes). En handler-timeout gör inget om beroendet ignorerar den.Ett klassiskt fel: du lägger till en 2-sekunders timeout i handlern, men ditt repository använder context.Background() för databasfrågan. Under load fortsätter en långsam query även efter att klienten gett upp, och ansamlingen växer.
Fixa grunderna: passera ctx som första argument genom din anropsstack. Inom långt arbete, lägg till snabba kontroller som select { case <-ctx.Done(): return ctx.Err() default: }. Mappa context.DeadlineExceeded till ett timeout-svar (ofta 504) och context.Canceled till ett klient-avbrott-svar (ofta 408 eller 499 beroende på era konventioner).
Timeouts hjälper bara om du kan se dem hända och bekräfta att systemet återhämtar sig snyggt. När något är långsamt ska förfrågan stoppas, resurser släppas och API:t förbli lyhört.
För varje förfrågan, logga samma lilla uppsättning fält så att du kan jämföra normala förfrågningar mot timeouter. Inkludera context-deadlinen (om den finns) och vad som avslutade arbetet.
Användbara fält inkluderar deadlinen (eller "none"), total förfluten tid, avbrottsorsak (timeout vs client canceled), en kort operationsetikett ("db.query users", "http.call billing") och ett request-ID.
Ett minimalt mönster ser ut så här:
start := time.Now()
deadline, hasDeadline := ctx.Deadline()
err := doWork(ctx)
log.Printf("op=%s hasDeadline=%t deadline=%s elapsed=%s err=%v",
"getUser", hasDeadline, deadline.Format(time.RFC3339Nano), time.Since(start), err)
Loggar hjälper dig debugga en förfrågan. Metriker visar trender.
Spåra några signaler som brukar skjuta i höjden tidigt när timeouter är fel: antal timeouter per route och beroende, pågående förfrågningar (ska plana ut under belastning), DB-pool väntetid och latens-percentiler (p95/p99) uppdelade på lyckade vs timeout.
Gör långsamheten förutsägbar. Lägg till en debug-only fördröjning i en handler, gör en DB-query långsammare med en avsiktlig väntan eller omslut ett externt anrop med en testserver som sover. Verifiera sedan två saker: du ser timeout-felet, och arbetet slutar snart efter avbrottet.
Ett litet load-test hjälper också. Kör 20–50 samtidiga förfrågningar i 30–60 sekunder med ett tvingat långsamt beroende. Goroutine-antalet och pågående förfrågningar ska stiga och sedan plana ut. Om de fortsätter klättra ignorerar något context-avbrott.
Timeouts hjälper bara om de tillämpas överallt där en förfrågan kan vänta. Innan du deployar, gör en genomgång av koden och kontrollera att samma regler följs i varje handler.
context.DeadlineExceeded och context.Canceled.http.NewRequestWithContext (eller req = req.WithContext(ctx)) och klienten har transport-timeouter (dial, TLS, response header). Undvik att lita på http.DefaultClient i produktionsvägar.En snabb "långsamt beroende"-övning före release är värd tiden. Lägg in en artificiell 2-sekunders fördröjning i en SQL-query och bekräfta tre saker: handlern returnerar i tid, DB-anropet stoppar faktiskt (inte bara handlern) och dina loggar säger tydligt att det var en DB-timeout.
Föreställ dig en endpoint som GET /v1/account/summary. En användaråtgärd triggar tre saker: en PostgreSQL-fråga (konto plus senaste aktivitet) och två externa HTTP-anrop (till exempel en betalningsstatuskontroll och en profil-förfiningsuppslagning).
Ge hela förfrågan en hård 2-sekunders budget. Utan en budget kan ett långsamt beroende hålla goroutines, DB-anslutningar och minne bundet tills ditt API börjar time-outa överallt.
En enkel fördelning kan vara 800ms för DB-frågan, 600ms för externt anrop A och 600ms för externt anrop B.
När du väl känner till den övergripande deadlinen, skicka ner den. Varje beroende får sin egen mindre timeout, men ärver fortfarande avbrottet från föräldern.
func AccountSummary(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
dbCtx, dbCancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer dbCancel()
aCtx, aCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer aCancel()
bCtx, bCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer bCancel()
// Use dbCtx for QueryContext, aCtx/bCtx for outbound HTTP requests.
}
Om externt anrop B saktar ner och tar 2.5 sekunder ska din handler sluta vänta vid 600ms, avbryta det pågående arbetet och returnera ett tydligt timeout-svar till klienten. Klienten ser ett snabbt fel istället för en snurrande väntindikator.
Dina loggar bör göra det uppenbart vad som tog budgeten, till exempel: DB klarade sig snabbt, extern A lyckades, extern B nådde sitt tak och returnerade context deadline exceeded.
När en verklig endpoint fungerar bra med timeouter och avbrott, gör det till ett återanvändbart mönster. Tillämpa det end-to-end: handler-deadline, DB-anrop och utgående HTTP. Kopiera sedan samma struktur till nästa endpoint.
Du går snabbare om du centraliserar det tråkiga: en boundary-timeout-helper, wrappers som säkerställer att ctx skickas till DB och HTTP-anrop, och en enhetlig felmappning och loggformat.
Om du vill prototypa detta mönster snabbt kan Koder.ai (koder.ai) generera Go-handlers och serviceanrop från en chatt-prompt, och du kan exportera källkoden för att applicera dina egna timeout-helpers och budgetar. Målet är konsekvens: långsamma anrop stoppas tidigt, fel ser likadana ut och felsökning beror inte på vem som skrev endpointen.
En långsam förfrågan håller kvar begränsade resurser medan den väntar: en goroutine, minne för buffrar och svarsobjekt, och ofta en databas- eller HTTP-anslutning. När tillräckligt många förfrågningar väntar samtidigt bildas köer, latensen ökar för all trafik och tjänsten kan börja misslyckas trots att varje enskild förfrågan till slut skulle bli klar.
Sätt en tydlig deadline vid gränsen för inkommande förfrågningar (proxy/gateway och i Go-servern), skapa en tidsatt context i handlern och skicka den ctx till alla blockerande anrop (databas och utgående HTTP). När deadlinen nås, returnera snabbt med ett konsekvent timeout-svar och stoppa pågående arbete som kan avbrytas.
Använd context.WithTimeout(parent, d) när du vill ”stoppa efter denna duration”, vilket är vanligast i handlers. Använd context.WithDeadline(parent, t) när du redan har en fast tidpunkt att förhålla dig till. Använd context.WithCancel(parent) när någon intern händelse ska stoppa arbetet tidigt, till exempel ”vi har redan svaret” eller ”klienten kopplade ner”.
Ring alltid cancel-funktionen, vanligtvis med defer cancel() direkt efter att du skapat den härledda contexten. Att avbryta frigör timern och ger alla barnarbete en tydlig stoppsignal, särskilt i kodvägar som returnerar tidigt innan deadlinen inträffar.
Skapa request-contexten en gång i handlern och skicka ner den som första argument till funktioner som kan blockera. Ett enkelt sätt att hitta problem är att söka efter context.Background() eller context.TODO() i request-kodvägar; de bryter ofta vidarebefordran av avbrott och deadline.
Använd context-medvetna databasmetoder som QueryContext, QueryRowContext och ExecContext (eller motsvarande i din driver). När contexten avslutas kan drivrutinen be Postgres att avbryta frågan så att du inte fortsätter bränna tid och anslutningar efter att förfrågan redan är över.
Fäst parent-request-contexten på det utgående anropet med http.NewRequestWithContext(ctx, ...) och konfigurera även klientens transport-tidsgränser så att du är skyddad under uppkoppling, TLS och väntan på responsrubriker. Stäng alltid response body även vid fel eller icke-200-svar så att anslutningar återgår till poolen.
Välj en total budget för förfrågan först, och ge sedan varje beroende en mindre del som får plats däri, med en liten buffert för handler-överhead och svarskodning. Om parent-contexten bara har liten tid kvar, starta inte kostsamma operationer som inte realistiskt kan bli klara före deadlinen.
Ett vanligt tillvägagångssätt är att mappa context.DeadlineExceeded till 504 Gateway Timeout med en kort text som “request timed out”. context.Canceled betyder ofta att klienten försvann; ofta är det bästa att stoppa arbetet och inte skriva en kropp, så att du inte slösar resurser.
Vanligaste felen är att tappa request-contexten genom att använda context.Background(), starta retry-slingor eller sleep utan att kolla ctx.Done(), och glömma att fästa ctx på blockerande anrop. Ett annat subtilt problem är att stapla många orelaterade tidsgränser som gör fel svåra att förstå och kan orsaka oväntade avbrott.