ProgrammazioneSviluppatore Go

Quali sono le caratteristiche del trattamento degli errori in Go, come creare e gestire correttamente gli errori, e come implementare tipi di errori personalizzati?

Supera i colloqui con l'assistente IA Hintsage

Risposta

Go è stato progettato sin dall'inizio attorno al ritorno esplicito degli errori anziché alle eccezioni. Questo consente di sviluppare codice prevedibile ed evitare insidie nascoste proprie delle costruzioni try/catch di altri linguaggi (ad esempio, Java o C++). Un errore in Go è un'interfaccia che implementa il metodo Error(), il che consente di creare errori sia semplici che complessi/involucrati con contesto.

I problemi sorgono quando gli errori non vengono gestiti correttamente (ad esempio, se vengono ignorati tramite _ o non vengono avvolti con informazioni aggiuntive per il debug) o nella creazione di errori "magici" che non corrispondono all'interfaccia error. È anche importante essere in grado di restituire errori dalle funzioni pubbliche e verificarli in ogni chiamata.

La soluzione è adottare approcci standard:

  • Restituire sempre l'errore come ultimo valore della funzione.
  • Utilizzare errors.New, fmt.Errorf per creare errori.
  • Creare tipi personalizzati per gli errori che implementano Error().
  • Applicare avvolgimenti di errori (errors.Wrap) per casi complessi, in modo da non perdere il contesto.

Esempio di codice:

import ( "errors" "fmt" ) type NotFoundError struct { Resource string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s non trovato", e.Resource) } func GetUser(id int) (string, error) { if id != 1 { return "", &NotFoundError{"Utente"} } return "Steve", nil } func main() { user, err := GetUser(2) if err != nil { if nfe, ok := err.(*NotFoundError); ok { fmt.Println(nfe.Resource, "problema") } else { fmt.Println("errore:", err) } } fmt.Println("utente:", user) }

Caratteristiche chiave:

  • Gli errori vengono sempre restituiti esplicitamente, solitamente come ultimo argomento della funzione.
  • Per errori complessi si possono definire propri tipi (struct) che implementano l'interfaccia error.
  • Per l'avvolgimento degli errori e la conservazione dello stack trace si può utilizzare il pacchetto "errors" (Go 1.13+ supporta errors.Is e errors.As per lavorare con la catena di errori).

Domande insidiose.

È possibile confrontare gli errori usando ==, o è necessario utilizzare metodi speciali?

È meglio utilizzare sempre errors.Is() per confrontare errori avvolti, altrimenti il confronto con == potrebbe non funzionare in caso di avvolgimento dell'errore.

if errors.Is(err, os.ErrNotExist) { // gestione dell'assenza del file }

È obbligatorio implementare un tipo-struct per un errore personalizzato, o è sufficiente utilizzare errors.New("...")?

Se è necessario solo il messaggio dell'errore, è sufficiente errors.New(). Se è importante mantenere il contesto (ad esempio, il nome della risorsa), è meglio definire una struct con il metodo Error().

Come restituire correttamente un errore nel caso di esecuzione riuscita della funzione?

In caso di successo, restituire sempre un errore nil.

return result, nil

Errori comuni e anti-pattern

  • Ignorare gli errori restituiti (tramite _ o saltando la gestione)
  • Confrontare gli errori solo con ==, nonostante gli errori annidati
  • Stringhe hardcoded invece di errori
  • Mancanza di contesto aggiuntivo negli errori

Esempio dalla vita reale

Caso negativo

Uno sviluppatore scrive una funzione che restituisce un errore senza spiegazioni (solo errors.New("fail")). Analizzando i log, non è possibile determinare la causa.

Vantaggi:

  • Rapidità di implementazione

Svantaggi:

  • Errori poco informativi
  • Complessità nel debug

Caso positivo

Uno sviluppatore definisce un tipo di errore personalizzato NotFoundError e lo restituisce con una spiegazione dettagliata.

Vantaggi:

  • Facilità di filtraggio dei log
  • Facilità di ricerca e gestione degli errori

Svantaggi:

  • Necessità di descrivere strutture aggiuntive