Saiba como lidar com erros no Go
Enquanto você está escrevendo seus programas, você precisa considerar as várias maneiras como seus programas podem falhar, e você precisa gerenciar falhas. Seus 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 que seus programas devem ser criados para lidar.
A abordagem de Go para lidar com erros é simplesmente um mecanismo de fluxo de controle onde apenas uma if
e uma return
instrução são necessárias. Por exemplo, quando você está chamando uma função para obter informações de um employee
objeto, talvez queira saber se o funcionário existe. A maneira opinativa de Go para lidar com um erro tão esperado seria assim:
employee, err := getInformation(1000)
if err != nil {
// Something is wrong. Do something.
}
Observe como a getInformation
função retorna o employee
struct e também um erro como um segundo valor. O erro pode ser nil
. Se o erro for nil
, isso significa sucesso. Se não nil
for, isso significa fracasso. Um não-erronil
vem com uma mensagem de erro que você pode imprimir ou, de preferência, registrar. É assim que você lida com erros no Go. Abordaremos algumas outras estratégias na próxima seção.
Você provavelmente notará que o tratamento de erros no Go exige que você preste mais atenção à forma como relata e lida com um erro. Esse é exatamente o ponto. Vejamos alguns outros exemplos para ajudá-lo a entender melhor a abordagem de Go para o tratamento de erros.
Usaremos o trecho de código que usamos para structs para praticar várias estratégias de tratamento de erros:
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
}
A partir daqui, vamos nos concentrar em modificar o getInformation
, apiCallEmployee
e main
funções para mostrar como lidar com erros.
Estratégias de tratamento de erros
Quando uma função retorna um erro, geralmente será o último valor de retorno. É responsabilidade do chamador verificar se existe um erro e lidar com ele, como você viu na seção anterior. 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) pode retornar o erro para o chamador sem fazer mais nada, assim:
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
}
Você também pode querer incluir mais informações antes de propagar o erro. Para isso, você pode usar a fmt.Errorf()
função, que é semelhante ao que vimos antes, mas retorna um erro. Por exemplo, você pode adicionar mais contexto ao erro e ainda retornar o erro original, desta 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, desta 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")
}
Finalmente, em vez de imprimir erros no console, você pode registrar erros e ocultar quaisquer detalhes de implementação dos usuários finais. Abordaremos o registro no próximo módulo. Por enquanto, vamos ver 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 para mensagens de erro comuns que deseja reutilizar. Em Go, você pode usar a errors.New()
função para criar erros e reutilizá-los em várias partes, como esta:
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 para a getInformation
função parece melhor, e se você precisar alterar a mensagem de erro, você fazê-lo em apenas um lugar. Além disso, observe que a convenção é incluir o prefixo Err
para variáveis de erro.
Finalmente, quando você tem uma variável de erro, você pode ser mais específico quando você está lidando com um erro em uma função de chamador. A errors.Is()
função permite que você compare o tipo de erro que você está recebendo, assim:
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
fmt.Printf("NOT FOUND: %v\n", err)
} else {
fmt.Print(employee)
}
Práticas recomendadas para tratamento de erros
Quando você estiver lidando com erros no Go, aqui estão algumas práticas recomendadas que você deve ter em mente:
- Verifique sempre se há erros, mesmo que não os espere. Em seguida, manipule-os corretamente para evitar a exposição de informações desnecessárias aos usuários finais.
- Inclua um prefixo em uma mensagem de erro para saber a origem do erro. Por exemplo, você pode incluir o nome do pacote e da função.
- Crie variáveis de erro reutilizáveis tanto quanto puder.
- Entenda a diferença entre usar erros de retorno e entrar em pânico. Entre em pânico quando não há mais nada que você possa fazer. Por exemplo, se uma dependência não estiver pronta, não faz sentido que o programa funcione (a menos que você queira executar um comportamento padrão).
- Registre erros com o maior número possível de detalhes (abordaremos como na próxima seção) e imprima erros que um usuário final possa entender.