Specifica della Gestione degli Errori in Golang

Tipi di Errori

Ci sono poche opzioni per dichiarare gli errori. Prima di scegliere l'opzione che meglio si adatta al tuo caso d'uso, considera quanto segue:

  • Il chiamante ha bisogno di corrispondere all'errore per gestirlo? Se sì, dobbiamo supportare le funzioni errors.Is o errors.As dichiarando variabili di errore di alto livello o tipi personalizzati.
  • Il messaggio di errore è una stringa statica o una stringa dinamica che richiede informazioni contestuali? Per le stringhe statiche, possiamo utilizzare errors.New, ma per le seconde, dobbiamo utilizzare fmt.Errorf o un tipo di errore personalizzato.
  • Stiamo passando nuovi errori restituiti dalle funzioni sottostanti? In tal caso, fare riferimento alla sezione sull'incapsulamento degli errori.
Corrispondenza Errore? Messaggio di Errore Linee guida
No statico errors.New
No dinamico fmt.Errorf
statico variabile di alto livello con errors.New
dinamico tipo di error personalizzato

Ad esempio, utilizzare errors.New per rappresentare errori con stringhe statiche. Se il chiamante ha bisogno di corrispondere e gestire questo errore, esportalo come variabile per supportare la corrispondenza con errors.Is.

Nessuna Corrispondenza degli Errori

// pacchetto foo

func Apri() error {
  return errors.New("impossibile aprire")
}

// pacchetto bar

if err := foo.Apri(); err != nil {
  // Impossibile gestire l'errore.
  panic("errore sconosciuto")
}

Corrispondenza degli Errori

// pacchetto foo

var ErroreImpossibileAprire = errors.New("impossibile aprire")

func Apri() error {
  return ErroreImpossibileAprire
}

// pacchetto bar

if err := foo.Apri(); err != nil {
  if errors.Is(err, foo.ErroreImpossibileAprire) {
    // gestire l'errore
  } else {
    panic("errore sconosciuto")
  }
}

Per gli errori con stringhe dinamiche, utilizzare fmt.Errorf se il chiamante non ha bisogno di corrispondere. Se il chiamante ha effettivamente bisogno di corrispondere, utilizzare un error personalizzato.

Nessuna Corrispondenza degli Errori

// pacchetto foo

func Apri(file string) error {
  return fmt.Errorf("file %q non trovato", file)
}

// pacchetto bar

if err := foo.Apri("testfile.txt"); err != nil {
  // Impossibile gestire l'errore.
  panic("errore sconosciuto")
}

Corrispondenza degli Errori

// pacchetto foo

type ErroreNonTrovato struct {
  File string
}

func (e *ErroreNonTrovato) Error() string {
  return fmt.Sprintf("file %q non trovato", e.File)
}

func Apri(file string) error {
  return &ErroreNonTrovato{File: file}
}

// pacchetto bar

if err := foo.Apri("testfile.txt"); err != nil {
  var nonTrovato *ErroreNonTrovato
  if errors.As(err, &nonTrovato) {
    // gestire l'errore
  } else {
    panic("errore sconosciuto")
  }
}

Si noti che se si esportano variabili o tipi di errore da un pacchetto, essi diventano parte dell'API pubblica del pacchetto.

Error Wrapping

Quando si verifica un errore durante la chiamata di un altro metodo, di solito ci sono tre modi per gestirlo:

  • Restituire l'errore originale così com'è.
  • Utilizzare fmt.Errorf con %w per aggiungere contesto all'errore e quindi restituirlo.
  • Utilizzare fmt.Errorf con %v per aggiungere contesto all'errore e quindi restituirlo.

Se non c'è alcun contesto aggiuntivo da aggiungere, restituire l'errore originale così com'è. Questo preserverà il tipo e il messaggio di errore originali. Questo è particolarmente adatto quando il messaggio di errore sottostante contiene informazioni sufficienti per tracciare da dove è originato l'errore.

In caso contrario, aggiungere contesto al messaggio di errore il più possibile in modo che non si verifichino errori ambigui come "connessione rifiutata". Invece, riceverai errori più utili, come "chiamata al servizio foo: connessione rifiutata".

Utilizza fmt.Errorf per aggiungere contesto ai tuoi errori e scegli tra i verbi %w o %v in base alla necessità che il chiamante possa corrispondere ed estrarre la causa principale.

  • Utilizza %w se il chiamante deve avere accesso all'errore sottostante. Questo è un buon valore predefinito per la maggior parte degli errori di wrapping, ma tieni presente che il chiamante potrebbe iniziare a fare affidamento su questo comportamento. Pertanto, per gli errori di wrapping che sono variabili o tipi noti, registrare e testarli come parte del contratto della funzione.
  • Utilizza %v per oscurare l'errore sottostante. Il chiamante non sarà in grado di corrispondere, ma potrai passare a %w in futuro se necessario.

Quando si aggiunge contesto all'errore restituito, evita di utilizzare frasi come "failed to" per mantenere il contesto conciso. Quando l'errore si diffonde attraverso lo stack, verrà accumulato strato su strato:

Non consigliato:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}

// failed to x: failed to y: failed to create new store: the error

Consigliato:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
// x: y: new store: the error

Tuttavia, una volta che l'errore viene inviato a un altro sistema, dovrebbe essere chiaro che il messaggio è un errore (ad es. un tag "err" o un prefisso "Failed" nei log).

Incorrect Naming

Per i valori di errore memorizzati come variabili globali, utilizzare il prefisso Err o err in base a se sono esportati. Si prega di fare riferimento alle linee guida. Per costanti e variabili non esportate di livello superiore, utilizzare un trattino basso (_) come prefisso.

var (
  // Esporta i seguenti due errori in modo che gli utenti di questo pacchetto possano corrisponderli con errors.Is.
  ErrLinkRotto = errors.New("il link è rotto")
  ErrImpossibileAprire = errors.New("impossibile aprire")

  // Questo errore non è esportato perché non vogliamo che faccia parte della nostra API pubblica. Possiamo comunque usarlo all'interno del pacchetto con errors.
  errNonTrovato = errors.New("non trovato")
)

Per i tipi di errore personalizzati, utilizzare il suffisso Error.

// Allo stesso modo, questo errore viene esportato in modo che gli utenti di questo pacchetto possano corrisponderlo con errors.As.
type ErroreNonTrovato struct {
  File string
}

func (e *ErroreNonTrovato) Error() string {
  return fmt.Sprintf("file %q non trovato", e.File)
}

// Questo errore non è esportato perché non vogliamo che faccia parte dell'API pubblica. Possiamo comunque usarlo all'interno di un pacchetto con errors.As.
type erroreRisoluzione struct {
  Percorso string
}

func (e *erroreRisoluzione) Error() string {
  return fmt.Sprintf("risoluzione %q", e.Percorso)
}

Gestione degli Errori

Quando il chiamante riceve un errore dal chiamato, può gestire l'errore in vari modi in base alla comprensione dell'errore.

Questo include, ma non è limitato a:

  • Abbinare l'errore con errors.Is o errors.As se il chiamato ha concordato su una definizione specifica dell'errore e gestire il branching in modi diversi
  • Registrare l'errore e degradare in modo delicato se l'errore è recuperabile
  • Restituire un errore ben definito se rappresenta una condizione di fallimento specifica del dominio
  • Restituire l'errore, che sia incapsulato o verbatim

Indipendentemente da come il chiamante gestisce l'errore, dovrebbe generalmente gestire ogni errore una sola volta. Ad esempio, il chiamante non dovrebbe registrare l'errore e poi restituirlo, in quanto il suo chiamante potrebbe gestire anche l'errore.

Ad esempio, considera i seguenti scenari:

Non Buono: Registrare l'errore e restituirlo

Altri chiamanti più in alto nello stack potrebbero prendere azioni simili su questo errore. Questo creerebbe molte informazioni all'interno dei log dell'applicazione con pochi benefici.

u, err := getUser(id)
if err != nil {
  // NON BUONO: Vedere descrizione
  log.Printf("Impossibile ottenere l'utente %q: %v", id, err)
  return err
}

Buono: Incapsulare l'errore e restituirlo

Gli errori più in alto nello stack si occuperanno di questo errore. Utilizzando %w assicura che possano abbinare l'errore con errors.Is o errors.As se rilevante.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("ottenere utente %q: %w", id, err)
}

Buono: Registrare l'errore e degradare in modo delicato

Se l'operazione non è assolutamente necessaria, possiamo fornire una degradazione delicata recuperandosi da essa senza interrompere l'esperienza.

if err := emitMetrics(); err != nil {
  // La mancata scrittura delle metriche non dovrebbe
  // interrompere l'applicazione.
  log.Printf("Impossibile emettere le metriche: %v", err)
}

Buono: Abbinare l'errore e degradare in modo appropriato

Se il chiamato ha definito un errore specifico nel suo accordo e il fallimento è recuperabile, abbinare quel caso di errore e degradare in modo delicato. Per tutti gli altri casi, incapsulare l'errore e restituirlo. Gli errori più in alto nello stack si occuperanno degli altri errori.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // L'utente non esiste. Utilizzare UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("ottenere utente %q: %w", id, err)
  }
}

Gestione delle Asserzioni Fallite

Le asserzioni di tipo causeranno un panic con un singolo valore restituito in caso di una determinazione errata del tipo. Pertanto, utilizzare sempre l'idioma "virgola, ok".

Non Raccomandato:

t := i.(string)

Raccomandato:

t, ok := i.(string)
if !ok {
  // Gestire l'errore in modo delicato
}

Evitare di utilizzare panic

Il codice in esecuzione nell'ambiente di produzione deve evitare di utilizzare panic. Il panic è la principale fonte di fallimenti a cascata. Se si verifica un errore, la funzione deve restituire l'errore e consentire al chiamante di decidere come gestirlo.

Non raccomandato:

func run(args []string) {
  if len(args) == 0 {
    panic("è richiesto un argomento")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Raccomandato:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("è richiesto un argomento")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Il panic/recover non è una strategia di gestione degli errori. Deve verificarsi un panic solo quando si verifica un evento non recuperabile (ad esempio, un riferimento nullo). Un'eccezione è durante l'inizializzazione del programma: le situazioni che potrebbero causare un panic del programma devono essere gestite durante l'avvio del programma.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Anche nel codice di test, è preferibile utilizzare t.Fatal o t.FailNow anziché panic per garantire che i fallimenti siano contrassegnati.

Non raccomandato:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("impossibile impostare il test")
}

Raccomandato:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("impossibile impostare il test")
}