Informazioni su come gestire gli errori in Go

Completato

Durante la scrittura di un programma, è necessario considerare i vari modi in cui li programma potrebbe generare errori e occorre gestire tali errori. Gli utenti non hanno bisogno di visualizzare messaggi di errore di analisi dello stack lunghi e fonte di confusione. Ha più senso mostrare loro informazioni significative su cosa non ha funzionato. Come si è già visto, Go offre funzioni predefinite come panic e recover per gestire le eccezioni, o i comportamenti imprevisti, nei programmi. Gli errori sono però operazioni non riuscite note che i programmi devono essere in grado di gestire.

L'approccio di Go alla gestione degli errori è semplicemente un meccanismo di flusso di controllo in cui sono necessarie solo un'istruzione if e un'istruzione return. Ad esempio, quando si chiama una funzione per ottenere informazioni da un oggetto employee, può essere opportuno verificare se il dipendente esiste. L'approccio di Go per la gestione di un tale errore previsto sarebbe simile al seguente:

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}

Come si può notare, la funzione getInformation restituisce lo struct employee e anche un errore come secondo valore. L'errore potrebbe essere nil. Se l'errore è nil, significa che l'operazione è riuscita. Se non è nil, significa che non è riuscita. Un errore non nil viene visualizzato con un messaggio di errore che può essere stampato o, preferibilmente, registrato. Questo è il modo in cui vengono gestiti gli errori in Go. Nella prossima sezione verranno illustrate alcune altre strategie.

Probabilmente si noterà che la gestione degli errori in Go richiede una maggiore attenzione al modo in cui gli errori vengono segnalati e gestiti. Questo è esattamente il punto. Verranno ora esaminati altri esempi per comprendere meglio l'approccio di Go alla gestione degli errori.

Si userà il frammento di codice usato per gli struct per fare pratica con le diverse strategie di gestione degli errori:

package main

import (
    "fmt"
    "os"
)

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee, err := getInformation(1001)
    if err != nil {
        // Something is wrong. Do something.
    } else {
        fmt.Print(employee)
    }
}

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

Da questo punto in poi ci si concentrerà sulla modifica delle funzioni getInformation, apiCallEmployee e main per illustrare come gestire gli errori.

Strategie di gestione degli errori

Quando una funzione restituisce un errore, in genere è l'ultimo valore restituito. Come illustrato nella sezione precedente, è responsabilità del chiamante controllare se esiste un errore e gestirlo. Una strategia comune consiste quindi nel continuare a usare tale criterio per propagare l'errore in una subroutine. Ad esempio, una subroutine (come getInformation nell'esempio precedente) potrebbe restituire l'errore al chiamante senza eseguire altre operazioni, come illustrato di seguito:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

Potrebbe essere utile includere anche altre informazioni prima di propagare l'errore. A tale scopo, si potrebbe usare la funzione fmt.Errorf(), che è simile al codice illustrato in precedenza ma restituisce un errore. Ad esempio, si potrebbe aggiungere più contesto all'errore pur continuando a restituire l'errore originale, come illustrato di seguito:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
    }
    return employee, nil
}

Un'altra strategia consiste nell'eseguire la logica di ripetizione dei tentativi quando gli errori sono temporanei. Ad esempio, si potrebbe usare un criterio di ripetizione per chiamare una funzione tre volte e attendere due secondi, come nel codice seguente:

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

Infine, anziché visualizzare gli errori nella console, è possibile registrare gli errori e nascondere tutti i dettagli di implementazione agli utenti finali. La registrazione verrà illustrata nel prossimo modulo. Per il momento, si esaminerà come creare e usare errori personalizzati.

Creare errori riutilizzabili

Nelle situazioni in cui il numero di messaggi di errore aumenta e si vuole mantenere l'ordine oppure si vuole creare una libreria per i messaggi di errore comuni da riutilizzare, in Go è possibile usare la funzione errors.New() per creare errori e riutilizzarli in diverse parti, come nel codice seguente:

var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
    if id != 1001 {
        return nil, ErrNotFound
    }

    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

Il codice per la funzione getInformation ha un aspetto migliore e, se occorre modificare il messaggio di errore, lo si fa in un'unica posizione. Si noti inoltre che la convenzione prevede l'inclusione del prefisso Err per le variabili di errore.

Infine, quando si dispone di una variabile di errore, è possibile essere più specifici nella gestione di un errore in una funzione del chiamante. La funzione errors.Is() consente di confrontare il tipo di errore che si è verificato, come nel codice seguente:

employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

Ecco alcune procedure consigliate da tenere presenti quando si gestiscono gli errori in Go:

  • Verificare sempre la presenza di errori, anche se non sono previsti, quindi gestirli in modo appropriato per evitare di esporre informazioni non necessarie agli utenti finali.
  • Includere un prefisso in un messaggio di errore in modo da identificare l'origine dell'errore. Si può ad esempio includere il nome del pacchetto e della funzione.
  • Creare il maggior numero possibile di variabili di errore riutilizzabili.
  • Comprendere la differenza tra l'uso della restituzione di errori e le generazione di condizioni di panico. Generare una condizione di panico quando non sono possibili altre soluzioni. Ad esempio, se una dipendenza non è pronta, non ha senso che il programma funzioni, a meno che non si voglia eseguire un comportamento predefinito.
  • Registrare gli errori con il maggior numero di dettagli possibile (si vedrà come nella prossima sezione) e visualizzare errori che un utente finale possa comprendere.