Go Context Package nel 2026: Cancellation, Timeout e Domande da Colloquio

Guida completa al package context di Go per gestire cancellation, deadline e valori request-scoped. Esempi pratici e domande frequenti nei colloqui tecnici.

Guida al Go Context Package per Cancellation e Timeout

Il package context di Go fornisce il meccanismo standard per trasportare deadline, segnali di cancellation e valori specifici delle request attraverso i confini delle API. Ogni applicazione Go in produzione utilizza context: handler HTTP, query al database, chiamate gRPC e worker in background dipendono tutti da esso per il graceful shutdown e la gestione dei timeout.

Essenziale per i Colloqui

Context è uno degli argomenti più frequentemente richiesti nei colloqui Go. Gli intervistatori si aspettano che i candidati sappiano spiegare la propagazione del context, dimostrare una corretta gestione della cancellation ed evitare errori comuni come la memorizzazione del context nelle struct.

L'Interfaccia Context e i Suoi Quattro Metodi

L'interfaccia Context definisce quattro metodi che ogni implementazione di context deve soddisfare. La comprensione di questi metodi costituisce la base per un utilizzo efficace del context.

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() restituisce un channel receive-only. Quando questo channel viene chiuso, ogni goroutine in ascolto riceve immediatamente il segnale. Il pattern <-ctx.Done() blocca fino al verificarsi della cancellation. Err() spiega il motivo: context.Canceled quando cancellato esplicitamente, oppure context.DeadlineExceeded quando un timeout o una deadline sono scaduti.

Creare Context con Background e TODO

Due funzioni creano context root: context.Background() e context.TODO(). Entrambe restituiscono context non-nil e vuoti che non vengono mai cancellati.

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() serve come parent per le request in arrivo, funzioni main e codice di inizializzazione. TODO() funge da placeholder durante il refactoring quando la sorgente corretta del context non è ancora stata determinata. Gli strumenti di analisi statica possono segnalare l'utilizzo di TODO() per revisione successiva.

Cancellation con WithCancel e WithCancelCause

La cancellation manuale permette alle goroutine parent di segnalare alle goroutine figlie che il lavoro deve terminare. Questo pattern appare nei worker pool, nei task in background e nelle implementazioni di graceful shutdown.

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, introdotto in Go 1.20, fornisce informazioni di errore più ricche rispetto al semplice WithCancel. La funzione context.Cause() recupera l'errore passato alla funzione cancel, facilitando il debugging in sistemi complessi.

Pronto a superare i tuoi colloqui su Go?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Timeout e Deadline per la Gestione delle Request

Gli handler HTTP, le chiamate al database e le request a API esterne dovrebbero sempre avere timeout. Il package context offre WithTimeout e WithDeadline per questo scopo. Entrambi creano context che si cancellano automaticamente quando il tempo scade.

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)
}

Il pattern defer cancel() garantisce che le risorse vengano rilasciate anche quando l'operazione si completa prima del timeout. Saltare questa chiamata causa un memory leak: la goroutine del timer del context rimane attiva fino alla scadenza del timeout.

Valori Request-Scoped con WithValue

WithValue associa dati specifici della request a un context. I casi d'uso comuni includono request ID, token di autenticazione e span di tracing. La documentazione ufficiale enfatizza l'uso di tipi personalizzati come chiavi per evitare collisioni.

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))
}

I valori del context formano una catena immutabile: ogni chiamata a WithValue crea un nuovo context che avvolge il parent. Le ricerche attraversano questa catena, rendendo i valori profondamente annidati più lenti da accedere. Vanno memorizzati solo dati specifici della request, non configurazioni dell'applicazione o parametri opzionali.

Utility WithoutCancel e AfterFunc

Go 1.21 ha aggiunto WithoutCancel per operazioni che devono completarsi indipendentemente dalla cancellation del parent, come i task di cleanup. AfterFunc pianifica una callback quando si verifica la cancellation del context.

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 preserva i valori del context parent ma ignora il suo segnale di cancellation. Questo pattern è adatto per logging, emissione di metriche e commit di database che devono avere successo indipendentemente dall'esito della request.

Domande e Risposte per Colloqui

La preparazione per un colloquio tecnico Go richiede la comprensione degli aspetti interni del context e delle best practice. Queste domande appaiono frequentemente nei colloqui presso aziende che utilizzano Go per servizi backend. Per ulteriore preparazione ai colloqui Go, consultare il modulo Domande sul Context Package.

Perché context dovrebbe essere il primo parametro?

Il team Go ha stabilito questa convenzione per rendere la propagazione del context visibile e consistente. Posizionare context come primo parametro segnala ai lettori che la funzione rispetta la cancellation. La libreria standard segue questo pattern: http.Request.Context(), database/sql.QueryContext() e grpc.UnaryInterceptor si aspettano tutti context come primo argomento.

Cosa succede se si memorizza context in una struct?

Memorizzare context rompe il modello del ciclo di vita della request. Una struct potrebbe sopravvivere alla request per cui è stata creata, causando operazioni che utilizzano segnali di cancellation obsoleti o perdono nuove deadline. La documentazione del context mette esplicitamente in guardia contro questo 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)
}

Come gestire context nelle goroutine?

Le goroutine devono controllare ctx.Done() nel loro loop principale o nella select statement. Ignorare il channel done crea goroutine leak: la funzione parent ritorna, ma la goroutine continua a consumare risorse.

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)
        }
    }
}

Quando utilizzare context.TODO()?

TODO() va utilizzato durante il refactoring incrementale quando si aggiunge supporto context a codice legacy. Marca le posizioni che necessitano di una corretta propagazione del context. Il codice in produzione dovrebbe eventualmente sostituire tutte le chiamate TODO() con context reali provenienti da handler delle request o inizializzazione dell'applicazione.

Errori Comuni da Evitare

L'uso improprio del context porta a bug sottili che si manifestano sotto carico o durante sequenze di shutdown. Riconoscere questi pattern previene incidenti in produzione.

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() {}

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Punti Chiave per l'Uso di Go Context in Produzione

  • Passare context come primo parametro alle funzioni, mai memorizzarlo nelle struct
  • Chiamare sempre cancel() con defer dopo WithTimeout, WithDeadline o WithCancel per prevenire memory leak
  • Utilizzare WithCancelCause e context.Cause() per un debugging migliore in catene di cancellation complesse
  • Controllare <-ctx.Done() nei loop delle goroutine per abilitare graceful shutdown e prevenire leak
  • Usare tipi di chiave personalizzati per WithValue per evitare collisioni tra package
  • Applicare WithoutCancel per operazioni di cleanup che devono completarsi indipendentemente dalla cancellation del parent
  • Preferire context.Background() per l'inizializzazione dell'applicazione e context.TODO() solo durante il refactoring
Sfida del giorno

Sapresti trovare il bug in Go?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 29 agosto 2026

Tag

#go
#golang
#context
#concurrency
#interview

Condividi

Articoli correlati