Hai mai visto una API Go che crasha silenziosamente perché un errore ignorato ha fatto saltare tutto? Succede. Lo vediamo ogni giorno nei progetti che ci arrivano in revisione: errori wrappati male, contesti persi, stack mai stampati. Il problema non è Go – è il modo in cui lo trattiamo.
Noi, di Meteora Web, lavoriamo con Go da anni per backend di clienti in tutta Italia. E abbiamo smesso da tempo di considerare l’error handling un ripensamento. In Go gli errori sono valori – non eccezioni che saltano fuori dal nulla. Questa guida ti mostra come sfruttare questa filosofia con i pattern moderni, dal wrapping all’ispezione, fino alla gestione in goroutine.
Perché in Go gli errori sono valori e non eccezioni?
Go non ha try-catch. Quando una funzione può fallire, restituisce un valore di tipo error come ultimo argomento. Sembra una scelta di base, ma cambia tutto: l’errore diventa un dato che puoi ispezionare, incapsulare, trasformare. Non è un evento esterno che interrompe il flusso – è parte del flusso stesso.
Pensa a un database call: non puoi fingere che la connessione sia sempre disponibile. Con le eccezioni il codice è lineare finché qualcosa non esplode a due livelli di distanza. Con gli errori-valori decidi esplicitamente cosa fare in ogni punto.
Sponsored Protocol
// Errore gestito come valore: controlli, decidi, procedi
func GetUser(id int) (*User, error) {
row := db.QueryRow("SELECT name, email FROM users WHERE id = ?", id)
err := row.Scan(&u.Name, &u.Email)
if err != nil {
return nil, fmt.Errorf("GetUser %d: %w", id, err)
}
return &u, nil
}
Il pattern if err != nil non è verboso: è un contratto. Ogni errore che ignori è un bug in potenza. Noi lo chiamiamo “responsabilità in superficie” – e nei nostri progetti non passano pull request che usano _ su un errore.
Errori sentinella: quando un errore è una costante
Un errore sentinella è una variabile globale di tipo error che rappresenta una condizione specifica. Esempio classico: io.EOF o sql.ErrNoRows. Si usa per confronti con == ma attenzione: se lo wrappi, il confronto fallisce. Per questo i pattern moderni usano errors.Is.
var ErrNotFound = errors.New("user not found")
func FindUser(id int) (*User, error) {
// ...
return nil, ErrNotFound
}
// Chiamante:
user, err := FindUser(42)
if errors.Is(err, ErrNotFound) {
// restituisci 404
}
Come gestire gli errori con il pattern if err != nil e quando evitarlo?
Il pattern base è semplice: ogni chiamata che può dare errore viene controllata subito. Ma ci sono casi in cui scrivere 20 if err != nil di fila diventa rumoroso. Ecco tre strategie per pulirlo senza perdere controllo.
Sponsored Protocol
1. Funzioni helper che accumulano errori
Se devi eseguire più operazioni che possono fallire singolarmente, usa uno slice di errori e poi controlli alla fine. Funziona bene per validazioni o per batch di scrittura indipendenti.
var errs []error
if err := step1(); err != nil {
errs = append(errs, fmt.Errorf("step1: %w", err))
}
if err := step2(); err != nil {
errs = append(errs, fmt.Errorf("step2: %w", err))
}
if len(errs) > 0 {
return errors.Join(errs...)
}
errors.Join (Go 1.20+) unisce più errori in uno solo che supporta errors.Is e errors.As su ogni componente.
2. Uso di closure per gestire errori comuni
In loop o chiamate ripetitive, crea una funzione anonima che centralizza la gestione. Esempio tipico: scrivere righe su un writer con chiusura differita.
var writeErr error
write := func(b []byte) {
if writeErr != nil {
return
}
_, writeErr = w.Write(b)
}
write([]byte("line1\n"))
write([]byte("line2\n"))
return writeErr
3. Quando non usare if err != nil: solo se l’errore è irrecuperabile
Se un errore è fatale e non puoi proseguire, panic è accettabile. Ma solo in fase di inizializzazione (config, database). Mai in logica di business. Noi di Meteora Web usiamo log.Fatal in entrypoint, mai in handler HTTP.
Sponsored Protocol
Quali pattern moderni per error wrapping e ispezione?
Il wrapping (aggiungere contesto a un errore) è obbligatorio se vuoi debugging utile. Con %w in fmt.Errorf crei una catena: l’errore originale rimane accessibile via errors.Is e errors.As.
func ReadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
// ...
}
Ora, nel chiamante, puoi chiedere:
if errors.Is(err, os.ErrNotExist) {
// file mancante
}
errors.Is percorre la catena di wrapping e confronta l’errore target. errors.As invece cerca un tipo specifico:
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Println("network error:", netErr.Op)
}
Error wrapping con formattazione aggiuntiva
Se hai bisogno di più contesto di quello che dà fmt.Errorf, crea un tipo personalizzato che implementi Unwrap():
Sponsored Protocol
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error { return e.Err }
Così mantieni il wrapping standard e puoi aggiungere campi strutturati utili per log e metriche.
Come progettare errori personalizzati e strutturati per debug e logging?
Gli errori in Go sono interfacce. Puoi quindi creare tipi che portano informazioni extra: codice HTTP, ID transazione, severità. Noi, di Meteora Web, usiamo errori strutturati in ogni API per avere log coerenti e debugging veloce.
Errore con codice e messaggio
type AppError struct {
Code int // 400, 404, 500
Message string
Err error // wrapped originale
}
func (e *AppError) Error() string {
return fmt.Sprintf("code %d: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Err }
Poi, in un middleware HTTP, fai il type switch con errors.As e restituisci il JSON giusto:
var appErr *AppError
if errors.As(err, &appErr) {
w.WriteHeader(appErr.Code)
json.NewEncoder(w).Encode(map[string]string{"error": appErr.Message})
return
}
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "internal server error"})
Logging con contesto
In produzione non puoi solo stampare err.Error(). Usa log/slog con attributi strutturati. Il tuo errore personalizzato può esporre un metodo LogValue() per apparire ordinato nei log.
Sponsored Protocol
func (e *AppError) LogValue() slog.Value {
return slog.GroupValue(
slog.Int("code", e.Code),
slog.String("message", e.Message),
slog.Any("cause", e.Err),
)
}
// Uso:
slog.Error("request failed", "error", appErr)
Come gestire errori in goroutine e canali senza perdere contesto?
Le goroutine che lanci e dimentichi sono la prima causa di bug silenziosi. La soluzione standard è usare un canale di errori o un WaitGroup con errore aggregato. Ecco il pattern che usiamo nei nostri backend.
Pattern base: canale di errori
errCh := make(chan error, 2)
go func() {
defer close(errCh)
// lavoro
if err := doWork(); err != nil {
errCh <- err
}
}()
if err := <-errCh; err != nil {
log.Error("goroutine failed", "error", err)
}
Attenzione al buffer: se non usi buffer o una goroutine lettrice, rischi deadlock. Preferisci buffer pari al numero di goroutine.
Pattern con WaitGroup e errore collettivo
Se devi aspettare più goroutine, usa sync.WaitGroup e raccogli errori in una variabile protetta da mutex, oppure usa errgroup dal package golang.org/x/sync/errgroup:
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
return step1(ctx)
})
g.Go(func() error {
return step2(ctx)
})
if err := g.Wait(); err != nil {
log.Error("group failed", "error", err)
}
Errgroup gestisce automaticamente il contesto: se una goroutine fallisce, il contesto viene cancellato e le altre possono abortire. Niente deadlock, niente errori persi.
Cosa fare adesso — error handling pratico per un’API REST in Go
Abbiamo visto la teoria: mettiamola in pratica. Ecco i 5 passi immediati che applichiamo in ogni progetto Meteora Web.
- Non usare mai più
_per ignorare errori. Ogni errore ignorato è un bug latente. Se proprio devi, commenta e logga. - Wrappa ogni errore con contesto:
fmt.Errorf("operazione: %w", err). Aggiungi funzione, ID, parametri. Il tempo di debugging si riduce del 70%. - Definisci errori sentinella per i casi attesi (user not found, duplicate key) e usa
errors.Isper confrontarli. - Crea un tipo errore strutturato per l’API con codice HTTP e messaggio. Fatto una volta, lo riusi ovunque.
- Usa errgroup per goroutine – togliti il mal di testa della sincronizzazione manuale.
Pronto? Prendi il tuo progetto Go, applica il wrapping mancante e sostituisci i panic con return di errori. Il tuo server ringrazierà – e i tuoi log anche.
Se vuoi approfondire tutto il contesto su Go per backend – concorrenza, API REST, microservizi – leggi la nostra guida pillar su Go. Lì trovi il quadro completo.