Go context.Context gjort rätt: Avbrott, tidsgränser och värden
Go-kontext är kontrollflöde, inte lagring.
Go:s context.Context är enkel nog att använda fel — och det är just det problemet.
De flesta Go-utvecklare lär sig de grundläggande reglerna snabbt: överför context som det första argumentet, kontrollera ctx.Done(), använd context.WithTimeout och överför aldrig nil.
func DoSomething(ctx context.Context) error {
// ...
}
Dessa regler är användbara, men de täcker bara den enkla delen. I produktionsmiljöer är context inte bara en parameterkonvention — det är kontrollplanet för livslängden hos en begäran.

Context berättar för arbete när det ska sluta, hur mycket tid som återstår, vilken avbrottsväg som togs och vilka begäran-specifika värden som behöver transporteras över API-gränser. Används den väl, förhindrar den läckande goroutiner, undviker onödigt arbete, sprider deadline-tider och gör tjänster enklare att stänga av. Används den dåligt, blir den en påse med dolda beroenden, falska globala variabler, glömda timeout-värden, läckande tidtagare och förvirrande avbrottsbeteende.
Den lite åsiktsstyrda versionen är denna: använd context för avbrott, deadline-tider och begäran-specifik metadata, och använd den inte som en behållare för beroenden.
Vad context är till för
Paketet context har tre huvuduppgifter — avbrott, deadline-tider och tidsgränser, samt begäran-specifika värden — och dessa tre uppgifter täcker allt det är designat för.
En context bör besvara frågor som:
Har denna begäran avbrutits?
Hur mycket tid har denna operation kvar?
Vilket begäran-ID ska kopplas till loggarna?
Vilken autentiserad användare är associerad med denna begäran?
En context bör inte besvara frågor som:
Var finns min databasanslutning?
Var finns min loggare?
Var finns min konfiguration?
Vilken tjänsteimplementering ska jag använda?
Det är beroenden — överför dem explicit genom funktionsparametrar (se Beroendeinjektion i Go för mönster på hur man gör detta rent). Context är för begäranens livslängd och begäranens metadata, inte för applikationstråddragning.
Den grundläggande formen av context
Det kärnaktiga gränssnittet är litet:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
De viktiga delarna är:
Done()stängs när contexten avbryts eller dess deadline löper ut.Err()förklarar varför contexten avslutades.Deadline()berättar om contexten har en deadline.Value()lagrar begäran-specifik data.
De flesta kodimplementerar inte detta gränssnitt. Den tar emot en context och överför den vidare.
Den första regeln: överför context explicit
För funktioner som utför begäran-specifikt eller avbrytbart arbete, överför context som den första parametern — detta är den standardkonventionen i Go och vad varje bibliotek och verktyg i ekosystemet förväntar sig:
func GetUser(ctx context.Context, id string) (*User, error) {
// ...
}
Gör detta för funktioner som kan:
- Anropa en databas
- Anropa en annan tjänst
- Vänta på en kö
- Starta bakgrundsarbete
- Blockeras vid I/O
- Använda en tidsgräns
- Behöver begäran-specifika värden
- Behöver avbrott
Lägg inte till context i små rena funktioner som inte behöver den.
Detta är okej:
func NormalizeEmail(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
Inte varje funktion behöver en context. Att lägga till context överallt gör koden bråkig.
Lagra inte context i strukturer
Att lagra en context i en struktur är ett av de vanligaste misstagen i Go-kodbasen, och det är värt att nämna explicit. Gör inte detta:
type UserService struct {
ctx context.Context
db *sql.DB
}
Gör detta istället:
type UserService struct {
db *sql.DB
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
// ...
}
En context tillhör en begäran, operation eller uppgift, medan en tjänststruktur oftast lever mycket längre än någon enskild begäran. Att blanda dessa livslängder gör avbrott otydligt och gör det svårt att resonera kring vilken operation en context tillhör.
Det finns sällsynta undantag för typer som genuint representerar en enkel operations livslängd, men de är tillräckligt sällsynta så att den grundregeln bör vara enkel:
Överför context. Lagra den inte.
Överför inte nil-context
Överför aldrig nil som en context.
Dåligt:
err := svc.DoWork(nil)
Använd context.Background() när det inte finns någon befintlig context:
err := svc.DoWork(context.Background())
I tester, använd testcontexten när det är möjligt:
func TestDoWork(t *testing.T) {
err := svc.DoWork(t.Context())
if err != nil {
t.Fatal(err)
}
}
En nil-context kan panikera när kod anropar metoder på den. En bakgrundskontext är explicit och säker.
Bakgrund-, TODO- och begäran-context
Det finns tre vanliga startpunkter.
context.Background
Använd context.Background() på toppnivån i ett program när ingen överordnad context finns — det är rotcontexten från vilken alla undercontexter härleds:
func main() {
ctx := context.Background()
_ = run(ctx)
}
eller:
func TestSomething(t *testing.T) {
ctx := context.Background()
_ = ctx
}
context.TODO
Använd context.TODO() när du vet att en context bör användas men inte har bestämt vilken än.
ctx := context.TODO()
Detta är användbart under migration, men det bör inte bli permanent om en verklig context finns.
Begäran-context
I HTTP-server, använd begärancontexten:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = ctx
}
Begärancontexten avbryts när klientanslutningen stängs, begäran avbryts eller servern är klar med att hantera begäran.
För webbtjänster är detta oftast contexten du bör överföra vidare till applikationskoden.
Avbrott med context.WithCancel
Använd context.WithCancel när du vill stoppa arbete explicit.
ctx, cancel := context.WithCancel(parent)
defer cancel()
Den returnerade cancel-funktionen avbryter undercontexten och frigör resurser associerade med den. Anrop alltid den när du är klar — även om contexten till slut kommer att löpa ut, undviker tidigt anrop av cancel att hålla resurser vid liv längre än nödvändigt.
Exempel:
func RunWorker(parent context.Context) error {
ctx, cancel := context.WithCancel(parent)
defer cancel()
done := make(chan error, 1)
go func() {
done <- doBackgroundWork(ctx)
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
Mönstret är enkelt:
- Härled en undercontext.
- Deferred cancel.
- Överför undercontexten till arbete som ska stoppa tillsammans.
- Övervaka
ctx.Done().
Timeout-värden med context.WithTimeout
Använd context.WithTimeout när en operation har en maximal varaktighet.
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
Exempel med en HTTP-klient:
func FetchUser(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Detta gör timeout-värdet en del av operationen, inte en dold global inställning.
Anrop alltid cancel
När du anropar WithCancel, WithTimeout eller WithDeadline, anrop alltid den returnerade cancel-funktionen — detta är viktigt för korrekthet.
Bra:
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
Dåligt:
ctx, _ := context.WithTimeout(parent, 5*time.Second)
Att misslyckas med att anropa cancel kan hålla tidtagare och undercontexter vid liv längre än nödvändigt.
Deadlines vs timeouts
En timeout är relativ:
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
En deadline är absolut:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(parent, deadline)
defer cancel()
De flesta applikationskoder använder timeouts. Deadlines är användbara när en begäran har en fast sluttid som ska delas över flera operationer — till exempel, om en begäran har 900 millisekunder kvar, ge inte varje nedströmsanrop en ny 1-sekunds timeout; sprid den återstående budgeten istället.
Timeout-budgeter över tjänstelager
Ett vanligt misstag är att stapla timeouts blint.
func Handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
_ = service.DoWork(ctx)
}
func (s *Service) DoWork(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.repo.Query(ctx)
}
Detta ser oskyldigt ut, men det döljer den verkliga budgeten. Tjänstelagret bör vanligtvis respektera anroparens deadline istället för att återställa tidtagaren till samma värde.
Ett bättre mönster är:
func Handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if err := service.DoWork(ctx); err != nil {
// hantera fel
return
}
}
Därefter inuti tjänsten:
func (s *Service) DoWork(ctx context.Context) error {
return s.repo.Query(ctx)
}
Lägg till en underordnad timeout endast när en deloperation behöver en mindre budget:
func (s *Service) DoWork(ctx context.Context) error {
queryCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
return s.repo.Query(queryCtx)
}
Den rätta mentala modellen är enkel: hela begäran har en yttre budget, specifika deloperationer kan ha mindre budgetar skurna ut från den budgeten, och inget lager utökar tyst begäran bortom vad anroparen avsåg.
Kontrollera ctx.Err() för att skilja avbrott från timeout
När en context slutar, returnerar ctx.Err() anledningen.
Vanligtvis är det ett av:
context.Canceled
context.DeadlineExceeded
Exempel:
select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultCh:
return handle(result)
}
Detta låter anropare skilja avbrott från timeout, och den skillnaden är viktig i praktiken. En avbruten begäran betyder ofta att klienten kopplades ur, medan ett deadline-överskridet fel vanligtvis betyder att din tjänst var för långsam — de bör inte alltid loggas, försökas igen eller rapporteras på samma sätt.
Använd context.Cause för bättre avbrottsanledningar
Modern Go stöder också orsaksmedvetet avbrott.
De användbara funktionerna inkluderar:
context.WithCancelCausecontext.WithTimeoutCausecontext.WithDeadlineCausecontext.Cause
Vanlig ctx.Err() berättar den breda anledningen: avbruten eller deadline överskriden.
context.Cause(ctx) kan berätta den mer specifika orsaken.
Exempel:
var ErrShutdown = errors.New("server shutting down")
func Run(ctx context.Context) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
go func() {
// Some shutdown signal arrived.
cancel(ErrShutdown)
}()
<-ctx.Done()
return context.Cause(ctx)
}
Använd orsaksmedvetet avbrott när anledningen är viktig för anropare, loggar eller städningsbeteende, och undvik det där en vanlig ctx.Err() räcker — den extra detaljen är bara värd det när diagnostik genuint kräver det.
HTTP-serverexempel
En normal HTTP-handläggare bör starta från r.Context(). För en full genomgång av att strukturera Go HTTP-tjänster, se Bygga REST-API:er i Go.
func GetUserHandler(svc *UserService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := r.PathValue("id")
user, err := svc.GetUser(ctx, id)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusOK, user)
}
}
Tjänsten bör acceptera och sprida contexten:
type UserService struct {
repo *UserRepository
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(ctx, id)
}
Repositoriet bör använda contextmedvetna databasmetoder:
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetUser(ctx context.Context, id string) (*User, error) {
const query = `
select id, email, name
from users
where id = $1
`
var user User
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.Name,
)
if err != nil {
return nil, err
}
return &user, nil
}
Det viktiga är kedjan — varje lager överför samma context vidare till nästa:
Bryt inte kedjan genom att skapa context.Background() i mitten.
context.Background()-misstaget: att bryta avbrottskedjan
Detta är en vanlig bugg:
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(context.Background(), id)
}
Detta kasserar all avbrotts- och deadlineinformation från anroparen. Om klienten kopplar ur, fortsätter databasfrågan att köras. Om begäran löper ut, kan nedströmsarbetet fortfarande vara igång. Om servern stängs av, ignorerar denna kod det helt. Att ersätta den mottagna contexten med context.Background() inuti affärslogik är nästan alltid fel.
Använd contexten du fick:
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(ctx, id)
}
Använd endast context.Background() vid kanten där ingen överordnad context finns.
HTTP-klientexempel
För utgående HTTP-begäran, koppla contexten till begäran.
func CallAPI(ctx context.Context, client *http.Client, endpoint string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Gör inte detta:
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
Detta skapar en begäran utan operationscontext.
Undvik också att enbart förlita dig på http.Client.Timeout. Det kan vara användbart som en säkerhetsgräns, men begärancontexter ger dig bättre spridning över anropskedjan.
Ett vanligt mönster är:
func CallAPI(ctx context.Context, client *http.Client, endpoint string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Använd detta när den nedströms API-anropet har en specifik budget inuti en större begäran.
Databasexempel
De flesta Go-databas-API:er har contextmedvetna metoder. För en bredare titt på hur Go-dataåtkomstbibliotek hanterar context — inklusive GORM, Ent, Bun och sqlc — se Jämföra Go ORM:er för PostgreSQL.
Använd dem.
Bra:
rows, err := db.QueryContext(ctx, query, args...)
Bra:
err := db.QueryRowContext(ctx, query, id).Scan(&name)
Bra:
result, err := db.ExecContext(ctx, query, args...)
Dåligt:
rows, err := db.Query(query, args...)
De contextmedvetna formerna tillåter databasoperationer att stoppa när begäran avbryts eller löper ut, vilket är särskilt viktigt för långsamma frågor, överbelastade databaser och användarvänliga API:er där latens direkt påverkar användarupplevelsen.
Transaktioner och context
Transaktioner kräver noggrann contexthantering.
En transaktion bör vanligtvis börja med operationscontexten:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
Använd sedan samma context för transaktionsoperationer:
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
Var försiktig med timeouts runt transaktioner. Om contexten avbryts innan Commit, kan transaktionen rullas tillbaka. Det kan vara vad du vill, men det bör vara avsiktligt.
För långa transaktioner är det bättre svaret oftast inte en längre timeout — det är en kortare transaktion som utför mindre arbete per enhet.
Bakgrundsarbetare och context
Bakgrundsarbetare bör ta emot en context som representerar deras livslängd.
Exempel:
type Worker struct {
logger *slog.Logger
}
func (w *Worker) Run(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := w.doOnce(ctx); err != nil {
w.logger.Error("worker iteration failed", "err", err)
}
}
}
}
Denna arbetare stannar rent när contexten avbryts, och dess tickare städas korrekt via defer ticker.Stop(). I main skulle du skapa en rotcontext kopplad till OS-signaler:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
worker := &Worker{logger: slog.Default()}
if err := worker.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
slog.Error("worker stopped", "err", err)
}
}
Detta är context använt korrekt: den beskriver processarbetets livslängd, och när OS:et skickar en signal, kommer hela trädet av goroutiner som delar denna context att stoppa tillsammans.
Förhindra läckande goroutiner med contextavbrott
En läckande goroutine händer när en goroutine förblir blockerad för alltid efter att den inte längre är användbar.
Context hjälper till att förhindra detta.
Dåligt:
func StartWorker() {
go func() {
for {
doWork()
time.Sleep(time.Second)
}
}()
}
Denna goroutine har ingen stängningsväg.
Bättre:
func StartWorker(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
doWork()
}
}
}()
}
Varje goroutine som looper bör nästan alltid ha en avbrottsväg.
Det betyder inte att varje goroutine måste ta emot context direkt, men systemet bör ha ett tydligt sätt att stoppa den.
context.AfterFunc
context.AfterFunc kör en funktion efter att en context avbrutits.
Det kan vara användbart för städning, att blockera upp operationer eller att broa API:er som inte nativt stöder context.
Exempel:
func waitWithContext(ctx context.Context, ch <-chan struct{}) error {
stop := context.AfterFunc(ctx, func() {
// Wake up or clean up if needed.
})
defer stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
Använd AfterFunc med försiktighet — den startar logik när avbrott inträffar, vilket kan göra kontrollflödet svårare att följa. För de flesta applikationskoder är ett normalt select på ctx.Done() tydligare och enklare att resonera kring. AfterFunc är mest värdefull när du behöver anpassa contextavbrott till ett API som inte redan accepterar context.
context.WithoutCancel
context.WithoutCancel skapar en context som inte avbryts när överordnade avbryts.
Detta är användbart, men det är också lätt att missanvända.
Exempel på användningsfall:
func Handler(audit *AuditLog) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Hantera begäran...
_ = ctx
auditCtx := context.WithoutCancel(ctx)
go func() {
ctx, cancel := context.WithTimeout(auditCtx, 2*time.Second)
defer cancel()
_ = audit.Write(ctx, "request completed")
}()
}
}
Idén är att revisionsskrivningen kan behöva fortsätta kort efter att begärancontexten avbrutits. Detta bör vara sällsynt och avsiktligt — använd inte WithoutCancel som ett sätt att undvika att hantera avbrott. Använd den endast när underarbetet genuint måste överleva det överordnade avbrottet, och lägg alltid till en ny timeout: en context som ignorerar avbrott men inte bär någon deadline kan lätt skapa bakgrundsgoroutine-läckor.
Contextvärden gjorda rätt
Contextvärden är för begäran-specifik data som korsar API-gränser.
Bra exempel:
- begäran-ID
- spårnings-ID
- autentiserat användar-ID
- tenant-ID
- språk/region
- säkerhetsprincip
- korrelationsmetadata
Dåliga exempel:
- databasanslutning
- loggare som ett dolt beroende
- funktionsflaggor för vanlig kontrollflöde
- valbara funktionsparametrar
- konfiguration
- tjänsteklienter
En användbar regel: om värdet är en del av begäranens identitet eller observabilitetscontext, kan det tillhöra context. Om det är ett beroende din kod behöver för att utföra sitt jobb, överför det explicit.
Använd typade nycklar för contextvärden
Använd inte vanliga strängar som contextnycklar.
Dåligt:
ctx = context.WithValue(ctx, "userID", "123")
Detta kan kollidera med andra paket.
Använd en oexporterad anpassad nyckeltyp:
type userIDKey struct{}
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey{}, userID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey{}).(string)
return userID, ok
}
Detta mönster ger dig typsäkerhet vid paketgränsen, undviker nyckelkollisioner med andra paket och håller context-API-ytan ren med typade åtkomstfunktioner.
Använd inte contextvärden för valbara parametrar
Detta är dåligt:
ctx = context.WithValue(ctx, "pageSize", 100)
users, err := repo.ListUsers(ctx)
Detta döljer funktionskontraktet.
Föredra explicita parametrar:
users, err := repo.ListUsers(ctx, ListUsersOptions{
PageSize: 100,
})
Contextvärden bör inte ersätta funktionsargument. Dolda indata gör koden svårare att förstå, testa och granska — och alla som läser funktionsignaturen kommer inte att ha en aning om att parametern ens existerar.
Loggning och context
Det finns två vanliga tillvägagångssätt för loggning med context. Exemplen här använder Go:s log/slog-paket — för en djupare dykning i strukturerad loggning med slog i produktionsmiljöer, se Strukturerad loggning i Go med slog.
Tillvägagångssätt 1: Extrahera värden och koppla dem till loggar
func LogRequest(ctx context.Context, logger *slog.Logger, msg string) {
if requestID, ok := RequestIDFromContext(ctx); ok {
logger = logger.With("request_id", requestID)
}
logger.Info(msg)
}
Detta håller loggaren explicit som ett ordentligt beroende och använder context endast för begäran-specifika värden som legitimt behöver korsa API-gränser.
Tillvägagångssätt 2: Lagra loggare i context
Vissa kodbasen lagrar en loggare i context.
Detta kan vara bekvämt, men jag rekommenderar det inte som standard. Det gör context till en beroendebehållare.
Min preferens:
- Överför loggarberoenden explicit.
- Lagra spårnings-ID:n och begäran-ID:n i context.
- Lägg till dessa värden i loggar vid gränser eller middleware.
Detta håller beroenden synliga.
Context och spårning
Spårning är ett av de starkaste användningsförfallen för contextvärden, och det är genuint bra lämpligt. OpenTelemetry och liknande system använder context för att sprida spårningsspännningar över funktionsanrop och processgränser, eftersom spårningsdata är exakt den typ av begäran-specifik metadata context var designad för att bära.
Ett typiskt mönster ser ut som:
func (s *Service) DoWork(ctx context.Context) error {
ctx, span := s.tracer.Start(ctx, "Service.DoWork")
defer span.End()
return s.repo.Query(ctx)
}
Contexten bär den aktiva spårningsspännningen, och repositoriet kan skapa en underordnad spännning från den. Varje lager lägger till sin egen spännning utan någon explicit överföring av tracer-objekt — contexten utför detta arbete transparent över hela anropsträdet.
Felhantering med context
När en operation stoppar på grund av contextavbrott, bevara den informationen. Mönstren här kompletterar de bredare felhanteringsstrategierna som täcks i Go-felhanteringsarkitektur.
Exempel:
err := svc.DoWork(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// Klienten avbröt eller anroparen stoppade arbetet.
return err
}
if errors.Is(err, context.DeadlineExceeded) {
// Timeout.
return err
}
return err
}
Dölj inte blint contextfel på ett sätt som döljer dem.
Inpackning med %w bevarar errors.Is, så anropare kan fortfarande upptäcka avbrott eller timeout:
if err != nil {
return fmt.Errorf("query user: %w", err)
}
Att ersätta felet helt kasserar den informationen och bryter varje anropare som kontrollerar specifika contextfeltyper:
if err != nil {
return errors.New("query user failed")
}
Kartlägga contextfel till HTTP-svar
Contextfel kartläggs ofta till olika HTTP-utfall.
Exempel:
func writeError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, context.Canceled):
// Klienten har troligen försvunnit.
// Vissa system loggar detta som en avbruten klientbegäran.
return
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
default:
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
}
Behandla inte klientavbrott som ett applikationsfel — om användaren stängde webbläsarfliken, är det inte din tjänst som beter sig fel, och att logga det som ett fel lägger till brus utan signal.
Context i middleware
HTTP-middleware är en vanlig plats att lägga till begäran-specifika värden.
Exempel på begäran-ID-middleware:
type requestIDKey struct{}
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey{}, requestID)
}
func RequestIDFromContext(ctx context.Context) (string, bool) {
requestID, ok := ctx.Value(requestIDKey{}).(string)
return requestID, ok
}
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 = newRequestID()
}
ctx := WithRequestID(r.Context(), requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Detta är en bra användning av context. Begäran-ID:t tillhör begäran, det bör resa genom hela anropskedjan, och att koppla det till loggar och spårningar på varje lager är exakt den typ av tvärsnittlig observabilitetsfråga som contextvärden är designade för att stödja.
Context i tester
I tester, undvik att använda context.Background() blint.
Föredra t.Context() när arbetet tillhör testets livslängd:
func TestService(t *testing.T) {
ctx := t.Context()
err := service.DoWork(ctx)
if err != nil {
t.Fatal(err)
}
}
För timeoutbeteende, testa med en verklig timeout endast om timeouten är liten och meningsfull.
För samtidighetskod och tidsberoende kod, överväg att använda testing/synctest — Testa samtidig Go-kod med synctest täcker detta verktyg i djupet:
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
time.Sleep(30 * time.Second)
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatalf("got %v, want deadline exceeded", ctx.Err())
}
})
}
Detta låter dig testa verkliga timeout-värden utan att vänta på verklig tid.
Context och errgroup
För grupper av goroutiner som ska avbrytas tillsammans, är errgroup ofta en bra passform.
Exempel:
func FetchAll(ctx context.Context, ids []string, client *Client) error {
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
id := id
g.Go(func() error {
_, err := client.Fetch(ctx, id)
return err
})
}
return g.Wait()
}
Om en goroutine returnerar ett fel, avbryts gruppcontexten och andra goroutiner som respekterar ctx.Done() kan stoppa i förtid. Detta är mycket renare än att manuellt hantera flera goroutiner, kanaler och avbrottsvägar. Den nyckelfrasen här är “respektera contexten” — errgroup kan inte stoppa arbete som ignorerar ctx.Done().
Graceful shutdown (smidig avstängning)
Context är centralt för smidig avstängning.
En typisk serveruppsättning har:
- en rotcontext avbruten av OS-signaler
- en HTTP-server
- bakgrundsarbetare
- en avstängningstid
- städningslogik
Exempel:
func main() {
root, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
server := &http.Server{
Addr: ":8080",
Handler: routes(),
}
go func() {
<-root.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
slog.Error("server shutdown failed", "err", err)
}
}()
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}
Observera att avstängningscontexten inte är samma som rotcontexten — roten är redan avbruten när OS-signalen ankommer. En separat timeout-context ger avstängningsprocessen en begränsad mängd tid att tömma pågående begäran innan tvångsavstängning, vilket är den subtila men viktiga distinktionen som gör att smidig avstängning faktiskt fungerar.
Vanliga antipattern
Antipattern 1: Använda context som en beroendebehållare
Dåligt:
ctx = context.WithValue(ctx, "db", db)
ctx = context.WithValue(ctx, "logger", logger)
ctx = context.WithValue(ctx, "config", cfg)
Överför beroenden explicit.
Antipattern 2: Att skapa context.Background inuti affärslogik
Dåligt:
func (s *Service) DoWork(ctx context.Context) error {
return s.repo.Save(context.Background())
}
Detta bryter avbrottspropageringen.
Antipattern 3: Att glömma cancel
Dåligt:
ctx, _ := context.WithTimeout(parent, time.Second)
Bra:
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
Antipattern 4: Att lägga valbara parametrar i context
Dåligt:
ctx = context.WithValue(ctx, "includeDeleted", true)
Använd explicita optionsstrukturer.
Antipattern 5: Att överföra context för djupt in i ren kod
Dåligt:
func Add(ctx context.Context, a, b int) int {
return a + b
}
Ren beräkning behöver inte context om den inte är långvarig eller avbrytbar.
Antipattern 6: Att ignorera avbrott i looper
Dåligt:
for item := range items {
process(item)
}
Bättre:
for item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := process(ctx, item); err != nil {
return err
}
}
Antipattern 7: Att svälja contextfel
Dåligt:
if err != nil {
return errors.New("operation failed")
}
Bra:
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
Bevara avbrotts- och deadline-fel.
En praktisk contextchecklista
Använd denna checklista för Go-backend-kod.
Funktionssignaturer
- Context är den första parametern.
- Context lagras inte i långlivade strukturer.
- Context överförs inte till rena hjälpfunktioner om det inte behövs.
- Nil-context används aldrig.
Avbrott
- Långvariga looper kontrollerar
ctx.Done(). - Goroutiner har en stängningsväg.
- Arbetarelivslängder är kopplade till en överordnad context.
- Contextavbrott sprids till nedströmsanrop.
Timeouts
- Yttre begäran-timeouts ställs in vid gränsen.
- Deloperation-timeouts är mindre än den yttre budgeten.
- Cancel-funktioner anropas alltid.
- Timeouts staplas inte blint på varje lager.
Värden
- Contextvärden är begäran-specifika.
- Nycklar använder anpassade typer, inte vanliga strängar.
- Beroenden lagras inte i context.
- Valbara parametrar lagras inte i context.
Fel
context.Canceledochcontext.DeadlineExceededbevaras.- Contextfel kartläggs korrekt vid API-gränser.
- Orsaksmedvetet avbrott används endast när anledningen är viktig.
Tester
- Tester använder
t.Context()där det är lämpligt. - Timeout-tester undviker långsamma verkliga väntetider.
- Samtidig timeout-beteende testas med
testing/synctestnär det är användbart. - Läckande goroutiner kontrolleras genom att säkerställa att stängningsvägar finns.
Hur man granskar contextanvändning i en Go-kodbas
Sök efter dessa mönster:
grep -R "context.Background()" .
grep -R "context.TODO()" .
grep -R "WithTimeout" .
grep -R "WithCancel" .
grep -R "WithValue" .
grep -R "type .* struct" .
Fråga sedan:
- Används
context.Background()endast vid toppnivågränser? - Anropas cancel-funktioner alltid?
- Placeras timeouts vid rimliga gränser?
- Är contextvärden verkligen begäran-specifika?
- Döljs beroenden i contextvärden?
- Kan goroutiner stoppas?
- Bevaras contextfel?
Detta är en bra kodgranskningsvanhet, eftersom många contextbuggar inte är syntaxbuggar — de är livslängdsbuggar som bara yttrar sig under avbrott, belastning eller avstängningsförhållanden.
Mina åsiktsstyrda regler
Dessa regler är tråkiga, men de fungerar.
Regel 1: Context är kontrollflöde
Använd context för att kontrollera avbrott, deadline-tider och begäranmetadata.
Använd den inte för att smugla beroenden.
Regel 2: Anroparen äger budgeten
En funktion bör vanligtvis respektera contexten den tar emot.
Skapa endast en kortare underordnad timeout när deloperationen behöver en specifik mindre budget.
Regel 3: Background tillhör kanten
Använd context.Background() i main, tester och toppnivåuppsättning.
Använd den inte inuti tjänst- och repositoriemetoder för att undvika avbrott.
Regel 4: Värden bör vara tråkiga
Begäran-ID, spårnings-ID, användar-ID och tenant-ID tillhör context. Databasanslutningar, loggrare, konfigurationsstrukturer och tjänsteklienter tillhör inte det — de är beroenden och bör överföras explicit.
Regel 5: Varje goroutine behöver en livslängd
Om en goroutine startar, bör du veta exakt hur den stoppar. Context är ofta rätt svar, och om det inte är context, bör det finnas någon annan tydlig mekanism — en kanal, en synkroniseringsprimtiv eller en explicit signal.
Avslutande tankar
context.Context är inte komplicerat eftersom API:t är stort — API:t är litet. Det är komplicerat eftersom det representerar livslängd, och livslängd är arkitektur. Varje beslut om var context flyter, var den härleds och var den stoppar är ett beslut om hur din tjänst hanterar fel, belastning och avstängning.
En väl använd context gör Go-tjänster enklare att avbryta, enklare att stänga av, enklare att observera och mindre benägna att läcka goroutiner. En dåligt använd context döljer beroenden, kasserar deadline-tider och gör koden svårare att resonera kring under press.
Den praktiska poängen är enkel:
Överför context vidare.
Lagra den inte.
Ersätt inte explicita parametrar med värden.
Respektera avbrott.
Använd timeouts vid gränser.
Anrop alltid cancel.
Det är Go-context gjord rätt.
Denna artikel är en del av App Architecture in Production-clustern, som täck kodstruktur, dataåtkomst, integrationsmönster och testarkitektur för produktionsmiljöer för Go- och Python-system.