Context
Che cosa e il Context?
Il pacchetto context in Go fornisce un meccanismo per trasportare scadenze, segnali di cancellazione e valori attraverso i confini delle API e delle goroutine. E uno dei pacchetti piu importanti per scrivere applicazioni Go robuste, specialmente quelle che gestiscono richieste HTTP, query al database o operazioni concorrenti.
context.Background()
context.Background() restituisce un context vuoto, non cancellabile. E il punto di partenza per creare context derivati e viene usato tipicamente nel main, nei test e come context di primo livello:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
fmt.Println(ctx) // context.Background
elabora(ctx)
}
func elabora(ctx context.Context) {
fmt.Println("Elaborazione con context:", ctx)
}
context.TODO()
context.TODO() e simile a Background() ma indica che non siamo ancora sicuri di quale context usare. E un segnaposto da sostituire in seguito:
func funzioneProvvisoria() {
// Usiamo TODO() quando non sappiamo ancora quale context passare
ctx := context.TODO()
elabora(ctx)
}
Usate TODO() solo durante lo sviluppo. Nel codice di produzione, dovrebbe essere sostituito con un context appropriato.
context.WithCancel
WithCancel crea un context derivato che puo essere cancellato manualmente chiamando la funzione cancel:
package main
import (
"context"
"fmt"
"time"
)
func generaNumeri(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
n := 0
for {
select {
case <-ctx.Done():
fmt.Println("Generatore fermato:", ctx.Err())
return
case ch <- n:
n++
time.Sleep(100 * time.Millisecond)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
numeri := generaNumeri(ctx)
// Leggi alcuni numeri
for i := 0; i < 5; i++ {
fmt.Println(<-numeri)
}
// Cancella il context
cancel()
time.Sleep(200 * time.Millisecond)
fmt.Println("Programma terminato")
}
E fondamentale chiamare sempre cancel() per rilasciare le risorse associate al context, anche se l’operazione termina normalmente. Il pattern defer cancel() e il modo piu sicuro.
context.WithTimeout
WithTimeout crea un context che si cancella automaticamente dopo una durata specificata:
package main
import (
"context"
"fmt"
"time"
)
func operazioneLenta(ctx context.Context) (string, error) {
select {
case <-time.After(3 * time.Second):
return "operazione completata", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Timeout di 1 secondo
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
risultato, err := operazioneLenta(ctx)
if err != nil {
fmt.Println("Errore:", err) // Errore: context deadline exceeded
return
}
fmt.Println(risultato)
}
context.WithDeadline
WithDeadline e simile a WithTimeout, ma specifica un momento preciso nel tempo anziche una durata:
package main
import (
"context"
"fmt"
"time"
)
func elaboraDati(ctx context.Context, dati []string) error {
for i, d := range dati {
select {
case <-ctx.Done():
return fmt.Errorf("elaborazione interrotta all'elemento %d: %w", i, ctx.Err())
default:
fmt.Printf("Elaboro: %s\n", d)
time.Sleep(500 * time.Millisecond)
}
}
return nil
}
func main() {
// Deadline: 1.5 secondi da adesso
deadline := time.Now().Add(1500 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
dati := []string{"alfa", "beta", "gamma", "delta", "epsilon"}
if err := elaboraDati(ctx, dati); err != nil {
fmt.Println("Errore:", err)
}
}
La differenza tra WithTimeout e WithDeadline e solo nella specifica: il timeout e relativo (durata), la deadline e assoluta (momento nel tempo). Internamente, WithTimeout usa WithDeadline.
context.WithValue
WithValue permette di associare coppie chiave-valore al context. E utile per trasportare dati specifici della richiesta come ID di tracciamento, informazioni di autenticazione o metadata:
package main
import (
"context"
"fmt"
)
// Definiamo un tipo personalizzato per la chiave (best practice)
type chiaveContext string
const (
chiaveUtente chiaveContext = "utente"
chiaveRichiesta chiaveContext = "id-richiesta"
)
func middleware(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, chiaveUtente, "mario.rossi")
ctx = context.WithValue(ctx, chiaveRichiesta, "req-12345")
return ctx
}
func handler(ctx context.Context) {
utente, ok := ctx.Value(chiaveUtente).(string)
if !ok {
fmt.Println("Utente non trovato nel context")
return
}
reqID, _ := ctx.Value(chiaveRichiesta).(string)
fmt.Printf("Richiesta %s dall'utente %s\n", reqID, utente)
}
func main() {
ctx := context.Background()
ctx = middleware(ctx)
handler(ctx)
}
Importante: usate sempre un tipo personalizzato (non string) per le chiavi del context, per evitare collisioni tra pacchetti diversi.
Passare il context nelle funzioni
Il context dovrebbe sempre essere il primo parametro di una funzione, chiamato ctx:
package main
import (
"context"
"fmt"
"time"
)
// Il context e sempre il primo parametro
func cercaUtente(ctx context.Context, id int) (string, error) {
select {
case <-time.After(200 * time.Millisecond):
return fmt.Sprintf("Utente-%d", id), nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func ottieniProfilo(ctx context.Context, id int) (string, error) {
utente, err := cercaUtente(ctx, id)
if err != nil {
return "", fmt.Errorf("errore nella ricerca utente: %w", err)
}
return fmt.Sprintf("Profilo di %s", utente), nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
profilo, err := ottieniProfilo(ctx, 42)
if err != nil {
fmt.Println("Errore:", err)
return
}
fmt.Println(profilo)
}
Best practice per il Context
Ecco le regole fondamentali per usare il context in modo corretto:
// 1. Il context e sempre il primo parametro
func buono(ctx context.Context, nome string) error { return nil }
// 2. Non memorizzate il context in una struct
// SBAGLIATO:
type ServizioCattivo struct {
ctx context.Context
}
// CORRETTO: passate il context ai metodi
type ServizioBuono struct{}
func (s *ServizioBuono) Esegui(ctx context.Context) error { return nil }
Altre regole importanti:
- Chiamate sempre
cancel()quando create un context conWithCancel,WithTimeoutoWithDeadline. - Non passate un context
nil: usatecontext.TODO()se non sapete quale context usare. - Usate
WithValuesolo per dati legati alla richiesta, non per parametri opzionali delle funzioni. - Il context e sicuro per l’uso concorrente tra piu goroutine.
- I context derivati vengono cancellati quando il context padre viene cancellato.
Conclusione
Il pacchetto context e essenziale per scrivere applicazioni Go robuste e reattive. Fornisce un modo standard per propagare cancellazioni, timeout e valori attraverso le API. Ricordate di passare sempre il context come primo parametro, di chiamare defer cancel() per ogni context derivato e di usare WithValue con parsimonia. Padroneggiare il context e fondamentale per sviluppare server HTTP, client di database e qualsiasi applicazione concorrente in Go.