É comum que as aplicações precisem lidar com situações excepcionais, como dados de entrada errados (json mal formatado), falhas de acesso ao disco ou em conexões de rede ou registro inexistente em uma base de dados. A manipulação de erros em Go é diferente de como realizamos em outras linguagens, como Java, Ruby e Python. Nessas linguagens, lidamos com exceções, que tratamos com o uso da estrutura try/catch ou algo similar.
Normalmente as pessoas recorrem à leitura de uma "stack trace" para identificar a linha de um arquivo em que a exceção surgiu, então, ao trabalhar com Go, buscam uma maneira similar através de bibliotecas de log, mas o resultado final são logs em várias partes do código, justamente por não utilizar a maneira própria da linguagem Go para o tratamento de erros.
Erros são valores
Em Go não temos uma sintaxe especial para manipular erros, porque erros são valores do tipo error, comumente retornados em uma função. Então, devemos verificar o erro e realizar algo: adicionar contexto, orientar o fluxo, etc.
1 2 3 4 5 6 7 8 | func Any() error { return errors.New("not found") }
// it was bad
} |
Os "sentinel errors"
Usamos um "sentinel error" para denotar um erro específico e realizar uma comparação com base em uma igualdade. São erros que encontramos em alguns pacotes, como sql.ErrNoRows.
1 2 3 4 5 | var ErrNotFound = errors.New("not found") if err == ErrNotFound { // ... } |
Usar um "sentinel error" para manipular um erro é pouco flexível, porque, antes de Go 1.13, perdemos o erro original, exceto o seu texto, ao criar um novo erro para adicionar contexto ao erro obtido.
1 2 3 | if err != nil { return fmt.Errorf("encrypt %v: %v", name, err) } |
O tipo "error"
Em Go, "error" é um tipo de interface com um método chamado Error() definido. Portanto, qualquer tipo que implementa a interface "error" é um erro.
1 2 3 4 5 6 7 | type AnyError struct {} func (e *AnyError) Error() string { return "something was bad" } if e, ok := err.(*AnyError); ok { // it was bad } |
Adicionar contexto a erros
Antes de Go 1.13
Como preservar o erro original ao adicionar contexto a um erro? Para a versão Go 1.12 (e anteriores), podemos usar o pacote github.com/pkg/errors, que tem duas funções principais: Wrap e Cause.
1 2 3 4 | // adds context to errors func Wrap(cause error, message string) error
// returns the cause of the error func Cause(err error) error |
O código seguinte é um exemplo de como podemos utilizar o pacote.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import ( "database/sql" "github.com/pkg/errors" ) type Finder interface { Find(id string) error } type UserRepository struct {} func (r *UserRepository) Find(id int) error { err := sql.ErrNoRows if err != nil { return errors.Wrap(err, "not found user") } return nil } |
No tipo UserUseCase definimos uma função, onde adicionamos contexto ao erro retornado pelo UserRepository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | type UserUseCase struct { UserRepository *UserRepository } func (u *UserUseCase) Find(id int) error { err := u.UserRepository.Find(id) if err != nil { msg := fmt.Sprintf("find user %d", id) return errors.Wrap(err, msg) } return nil } func main() { repo := UserUseCase{UserRepository: &UserRepository{}} fmt.Println(repo.Find(1)) } |
A saída será: “find user 1: not found user: sql: no rows in result set”, que pode ser enviado para um centralizador de logs.
Em Go 1.13+
Go 1.13+ introduz novas funcionalidades aos pacotes errors e fmt da biblioteca padrão para simplificar a manipulação de erros. A função fmt.Errorf suporta um novo verbo %w. Quando presente, o erro retornado terá um método Unwrap que retorna o argumento de %w, que deve ser um erro.
1 2 3 4 | f err != nil { // return an error which unwraps to err. return fmt.Errorf("encrypt %v: %w", name, err) } |
Segue um código de exemplo para utilizar as novas funcionalidades de Go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "fmt" "database/sql" ) type Finder interface { Find(id string) error } type UserRepository struct {} func (r *UserRepository) Find(id int) error { err := sql.ErrNoRows if err != nil { return fmt.Errorf("not found user: %w", err) } return nil } type UserUseCase struct { UserRepository *UserRepository } func (u *UserUseCase) Find(id int) error { err := u.UserRepository.Find(id) if err != nil { return fmt.Errorf("find user %d: %w", id, err) } return nil } func main() { repo := UserUseCase{UserRepository: &UserRepository{}} fmt.Println(repo.Find(1)) } |
A saída será: “find user 1: not found user: sql: no rows in result set”. Com poucos ajustes podemos alterar o código que utiliza o pacote github.com/pkg/errors para usar a função fmt.Errorf.
Verificação de erros
Ao utilizar o pacote github.com/pkg/errors, antes de Go 1.13, temos a função Cause, que nos permite saber se um determinado erro é a origem de outro erro.
1 | fmt.Println(errors.Cause(err) == sql.ErrNoRows) // true |
Em Go 1.13+, para comparar um erro a um valor, utilizamos a função Is.
1 2 3 4 5 6 7 8 9 10 | import ( // ... "errors" ) // ...
func main() { repo := UserUseCase{UserRepository: &UserRepository{}} err := repo.Find(1) fmt.Println(errors.Is(err, sql.ErrNoRows)) // true } |
Conclusão
É importante entender a maneira simples como Go em seu design decidiu tratar erros como valor. O resultado final de ter um contexto adicionado a um erro é uma maior facilidade de identificação da origem do problema, ainda que não esteja disponível a linha exata do código onde o erro ocorreu.
Comentários
Postar um comentário