f in x
Error Handling in Go — Errori Come Valori e Pattern Moderni per API Che Non Si Rompono
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sviluppo di siti web

Error Handling in Go — Errori Come Valori e Pattern Moderni per API Che Non Si Rompono

[2026-07-11] Author: Ing. Calogero Bono
Zenithby Meteora Web Il sistema operativo della tua attività. Social, clienti, prenotazioni e fatture in un'unica piattaforma. Palestre, barber, professionisti. Scopri Zenith Demo gratis · senza carta

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.

  1. Non usare mai più _ per ignorare errori. Ogni errore ignorato è un bug latente. Se proprio devi, commenta e logga.
  2. Wrappa ogni errore con contesto: fmt.Errorf("operazione: %w", err). Aggiungi funzione, ID, parametri. Il tempo di debugging si riduce del 70%.
  3. Definisci errori sentinella per i casi attesi (user not found, duplicate key) e usa errors.Is per confrontarli.
  4. Crea un tipo errore strutturato per l’API con codice HTTP e messaggio. Fatto una volta, lo riusi ovunque.
  5. 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.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()