Menu

Gestione degli errori in Golang: if err != nil, wrapping e controlli

Go gestisce gli errori come valori normali restituiti dalle funzioni. Scopri l'interfaccia error, if err != nil, errors.New e fmt.Errorf, come restituire errori con contesto, come controllarli con errors.Is ed errors.As e come gestire ogni errore una sola volta.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Gli errori sono valori

Una funzione Go che può fallire restituisce un error come ultimo risultato. Il chiamante lo controlla subito.

Output:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

Questo è tutto il meccanismo. Non ci sono eccezioni, né try o catch, né un flusso di controllo nascosto: un errore arriva solo dove il tuo codice lo passa. Il prezzo è la ripetizione visibile di if err != nil. Il vantaggio è che ogni punto di fallimento è visibile nella pagina, e in ognuno decidi tu cosa succede.

Il tipo error

error è un'interfaccia integrata con un solo metodo:

type error interface {
	Error() string
}

Qualsiasi tipo con un metodo Error() string è un errore. Un errore nil significa successo. Stampare un errore con fmt.Println(err) o %v chiama Error().

Creare errori

Due funzioni coprono la maggior parte dei casi.

errors.New crea un errore con un testo fisso. fmt.Errorf ne formatta uno, con gli stessi verbi di Printf. Per convenzione le stringhe di errore iniziano in minuscolo e non hanno punteggiatura finale, perché di solito vengono incluse in messaggi più lunghi: load config: open app.yaml: no such file or directory.

Lo schema if err != nil

La forma idiomatica è: chiama, controlla, ritorna subito. Il percorso di successo resta al margine sinistro e ogni fallimento esce appena si verifica.

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

Due convenzioni da notare:

  • In caso di errore, restituisci il valore zero per gli altri risultati (nil, 0, ""). I chiamanti non devono usarli quando err != nil.
  • if err := f(); err != nil limita la visibilità di err all'if quando la funzione restituisce solo un errore. Mantiene pulito lo scope esterno.

Evita l'else dopo un return di errore. if err != nil { return err } else { ... } indenta il percorso felice senza motivo.

Aggiungere contesto quando restituisci un errore

Un errore passato verso l'alto senza modifiche perde la storia di dove è nato. open config.yaml: no such file or directory non ti dice quale passo dell'avvio è fallito. Aggiungi contesto con fmt.Errorf e il verbo %w:

Output:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

Ogni livello aggiunge ciò che stava facendo, e il messaggio finale si legge come una traccia che va dalla cima della chiamata fino alla causa. Un buon contesto indica l'operazione e l'input: parse line 12, fetch user 42. Non aggiungere "error" o "failed" a ogni livello; il messaggio è già un errore.

%w avvolge: conserva l'errore originale dentro quello nuovo, così errors.Is ed errors.As possono ancora trovarlo. %v copia solo il testo. Usa %v quando vuoi deliberatamente nascondere ai chiamanti un dettaglio di implementazione, per esempio perché non finiscano per dipendere dal tipo di errore di un driver di database.

Controllare errori specifici: errors.Is ed errors.As

A volte il chiamante deve reagire a un tipo di fallimento preciso: un file mancante significa "usa i valori predefiniti", un timeout significa "riprova". Due funzioni rispondono a questa esigenza, ed entrambe guardano attraverso ogni livello di wrapping.

Regole pratiche:

  • Confronta con valori di errore predefiniti (sentinelle come io.EOF, os.ErrNotExist, sql.ErrNoRows) usando errors.Is, non ==. == fallisce appena l'errore viene avvolto.
  • Estrai un errore tipizzato con errors.As, non con un'asserzione di tipo, per lo stesso motivo. errors.As riceve un puntatore a una variabile del tipo di destinazione.
  • Non confrontare mai il testo di err.Error(). I messaggi cambiano tra una versione e l'altra, e il confronto sul testo si rompe silenziosamente quando succede.

Definire sentinelle e tipi di errore propri, e unire più errori con errors.Join, è trattato negli errori personalizzati.

Gestisci un errore una sola volta

Un errore va gestito esattamente una volta. Gestirlo significa una di queste cose: restituirlo (di solito avvolto), registrarlo nel log e continuare, riprovare, oppure trasformarlo in una risposta per l'utente. Farne due è il bug più comune legato agli errori nel codice Go.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

Registrare e restituire produce lo stesso fallimento più volte nei log, ogni volta con meno contesto del messaggio finale. Lascia che gli errori risalgano fino al punto che può decidere cosa fare (un handler HTTP, un main, un ciclo di un worker) e registrali lì.

Dove finiscono gli errori

In cima al programma qualcosa deve agire sull'errore. In main di solito significa stamparlo e uscire con uno stato diverso da zero:

Eseguirlo senza argomenti stampa error: usage: app <name> su stderr ed esce con stato 1 (scrivi un nome nel pannello Args per vedere l'altro percorso). Mantenere main in questa forma, con il lavoro vero dentro run, rende il programma testabile e fa sì che le istruzioni defer dentro run vengano comunque eseguite, dato che os.Exit salta le chiamate differite.

In un server HTTP la cima è l'handler: traduce l'errore in un codice di stato e in un messaggio sicuro per il client, e registra per te il messaggio dettagliato.

Errori che puoi ignorare e quelli che non devi ignorare

Ignorare un errore a volte è corretto, ma rendilo esplicito con _ così chi legge sa che è stata una scelta:

_ = conn.SetDeadline(t) // best effort

Alcune chiamate in pratica non possono fallire (strings.Builder.WriteString, bytes.Buffer.Write). Altre sembrano innocue e non lo sono: Close su un file che hai scritto può segnalare che i dati non sono mai arrivati sul disco, e json.Marshal fallisce su channel e funzioni. Nel dubbio, controlla.

Il linter errcheck (incluso in golangci-lint) segnala gli errori non controllati. go vet da solo non li segnala.

Errori e panic

Go ha anche panic, ma non è un sistema di eccezioni. Usa gli errori per tutto ciò che può andare storto durante il normale funzionamento: input non validi, file mancanti, problemi di rete. Riserva il panic ai bug (uno stato impossibile, un'invariante violata) e ai fallimenti all'avvio in cui continuare non ha senso. Una libreria non dovrebbe quasi mai andare in panic attraverso la sua API. Consulta panic e recover.

Ridurre la ripetizione

if err != nil è prolisso, e le proposte di aggiungere una nuova sintassi per questo sono state respinte più volte; nel 2025 il team di Go ha annunciato che non persegue più modifiche alla sintassi per la gestione degli errori. Alcuni schemi riducono il rumore restando dentro il linguaggio:

  • Ritorna presto e tieni le funzioni piccole. Gran parte della ripetizione viene da funzioni lunghe che fanno molti passaggi.
  • L'errore persistente. Per una sequenza di scritture, conserva il primo errore in un campo di una struct e fai sì che le chiamate successive non facciano nulla una volta impostato. bufio.Writer funziona così: controlli l'errore una volta sola dopo Flush.
  • Avvolgi una volta per funzione. Una closure differita su un risultato con nome può aggiungere lo stesso contesto a ogni errore che la funzione restituisce (vedi defer).

Errori comuni

  • Usare un valore quando err non è nil. Prima controlla, poi usa.
  • Registrare e restituire. Scegline uno.
  • Confrontare con == dopo il wrapping. Usa errors.Is.
  • Perdere la causa con %v. Usa %w, a meno che nasconderla non sia proprio lo scopo.
  • Restituire un puntatore nil tipizzato come error. var e *MyErr; return e per il chiamante non è nil. Restituisci un nil letterale.
  • Messaggi con maiuscola iniziale o punteggiatura. errors.New("Failed to connect.") si legge male una volta avvolto. Scrivi connect to db: ....

Domande frequenti

Come funziona la gestione degli errori in Go?

Le funzioni che possono fallire restituiscono un error come ultimo risultato. Il chiamante lo controlla subito: v, err := f(); if err != nil { return err }. Un error è un normale valore di interfaccia con un solo metodo, Error() string, e nil significa successo. Non ci sono eccezioni.

Go ha try/catch?

No. Go non ha eccezioni né try/catch. I fallimenti previsti vengono restituiti come valori error e controllati con if err != nil. panic e recover esistono, ma servono per i bug di programmazione e gli stati irrecuperabili, non per il normale flusso degli errori.

Come restituisco un errore in Go?

Dichiara error come ultimo risultato e restituisci nil in caso di successo. Crea gli errori con errors.New("message") per un testo fisso oppure con fmt.Errorf("reading %s: %w", name, err) per aggiungere contesto a un errore che hai ricevuto. In caso di fallimento, restituisci valori zero per gli altri risultati.

Qual è la differenza tra %w e %v in fmt.Errorf?

Entrambi inseriscono il messaggio dell'errore originale in quello nuovo. %w inoltre lo avvolge, così errors.Is ed errors.As possono ancora trovare l'originale. %v produce un nuovo errore con solo il testo. Usa %w quando i chiamanti potrebbero dover controllare la causa, e %v quando vuoi nasconderla.

Come controllo quale errore è stato restituito in Go?

Usa errors.Is(err, target) per confrontare con un errore sentinella come io.EOF o os.ErrNotExist, ed errors.As(err, &target) per estrarre un tipo di errore specifico come *fs.PathError. Entrambi attraversano gli errori avvolti. Evita di confrontare le stringhe di err.Error().

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA