Saiba como tratar erros no Go
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)
}
Práticas recomendadas para o tratamento de erro
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.