Saiba como tratar erros no Go

Concluído

Enquanto escreve programas, você precisa considerar as várias maneiras como eles podem falhar e precisa gerenciar as falhas. Os usuários não precisam ver um erro de rastreamento de pilha longo e confuso. É melhor que eles vejam informações significativas sobre o que deu errado. Como você viu, o Go tem funções internas como panic e recover para gerenciar exceções ou comportamento inesperado em seus programas. Mas os erros são falhas conhecidas com as quais seus programas devem ser capazes de lidar.

A abordagem do Go ao tratamento de erro é simplesmente um mecanismo de fluxo de controle no qual apenas um if e uma instrução return são necessários. Por exemplo, ao chamar uma função para obter informações de um objeto employee, talvez você queira saber se o funcionário existe. A maneira dogmática do Go de lidar com esse erro esperado seria semelhante a esta:

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

Observe como a função getInformation retorna o struct employee e também um erro como um segundo valor. O erro pode ser nil. Se o erro for nil, isso significará êxito. Se não for nil, significará falha. Um erro diferente de nil é acompanhado de uma mensagem de erro que você pode imprimir ou, preferencialmente, registrar. É assim que você lida com erros no Go. Abordaremos algumas outras estratégias na próxima seção.

Você provavelmente observará que o tratamento de erro no Go exige que você preste mais atenção a como relata e trata um erro. É exatamente esse o ponto. Vamos ver alguns outros exemplos que ajudarão você a entender melhor a abordagem do Go ao tratamento de erro.

Usaremos o snippet de código que usamos para os structs para praticar várias estratégias de tratamento de erro:

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
}

Daqui em diante, nos concentraremos em modificar as funções getInformation, apiCallEmployee e main para mostrar como tratar erros.

Estratégias de tratamento de erro

Quando uma função retorna um erro, geralmente ele é o último valor retornado. Como vimos na seção anterior, é responsabilidade do chamador verificar se existe um erro e tratá-lo. Portanto, uma estratégia comum é continuar usando esse padrão para propagar o erro em uma sub-rotina. Por exemplo, uma sub-rotina (como getInformation no exemplo anterior) poderia retornar o erro para o chamador sem fazer mais nada, da seguinte forma:

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
}

Talvez você também queira incluir mais informações antes de propagar o erro. Para essa finalidade, você pode usar a função fmt.Errorf(), que é semelhante ao que vimos anteriormente, mas retorna um erro. Por exemplo, você pode adicionar mais contexto ao erro e ainda retornar o erro original, da seguinte forma:

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
}

Outra estratégia é executar a lógica de repetição quando os erros são transitórios. Por exemplo, você pode usar uma política de repetição para chamar uma função três vezes e aguardar dois segundos, da seguinte forma:

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")
}

Por fim, em vez de imprimir erros no console, você pode registrá-los e ocultar os detalhes da implementação dos usuários finais. Abordaremos o registro em log no próximo módulo. Por enquanto, vamos dar uma olhada em como você pode criar e usar erros personalizados.

Criar erros reutilizáveis

Às vezes, o número de mensagens de erro aumenta e você deseja manter a ordem. Ou talvez você queira criar uma biblioteca com mensagens de erro comuns que deseja reutilizar. No Go, você pode usar a função errors.New() para criar erros e reutilizá-los em várias partes, da seguinte maneira:

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
}

O código da função getInformation parece melhor e, se você precisar alterar a mensagem de erro, fará isso em apenas um lugar. Além disso, observe que a convenção é incluir o prefixo Err para variáveis de erro.

Por fim, quando tiver uma variável de erro, você poderá ser mais específico quando estiver tratando um erro em uma função de chamador. A função errors.Is() permite que você compare o tipo de erro que está obtendo, desta forma:

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

Ao tratar erros no Go, estas são algumas práticas recomendadas a ter em mente:

  • Sempre verifique se há erros, mesmo que você não espere que haja. Depois, trate-os adequadamente para evitar a exposição de informações desnecessárias aos usuários finais.
  • Inclua um prefixo na mensagem de erro para que você saiba qual é a origem do erro. Por exemplo, você pode incluir o nome do pacote e da função.
  • Crie variáveis de erro reutilizáveis sempre que possível.
  • Conheça a diferença entre usar erros de retorno e entrar em pane. Entre em pânico quando não houver mais nada que você possa fazer. Por exemplo, se uma dependência não está pronta, não faz sentido que o programa funcione (a menos que você queira executar um comportamento padrão).
  • Registre erros com o máximo de detalhes possível (abordaremos como na próxima seção) e imprima erros que o usuário final possa entender.