00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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 con WithCancel, WithTimeout o WithDeadline.
  • Non passate un context nil: usate context.TODO() se non sapete quale context usare.
  • Usate WithValue solo 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.