Pular para o conteúdo principal

Tratamento de erros em Go


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

// ...
if err := Any(); err != nil {
    // 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

Postagens mais visitadas deste blog

Injeção de dependência em Go com fx

A injeção de dependência é um padrão de design de software utilizado por alguns frameworks, como Angular e Spring, que nos permite a inversão de controle entre tipos por meio do uso de uma abstração em vez de uma implementação, o que resulta em um código mais modular e fácil de testar. Em Go, o pacote fx, um sistema de injeção de dependência, permite-nos construir uma aplicação sem a necessidade de  passar objetos para cada chamada de construtor.  Mas uma estrutura organizada de pastas é importante para facilitar o desenvolvimento de um projeto, e o seguinte código busca mostrar um uso básico de fx e uma estrutura inicial de pastas que serve como exemplo de uso do sistema de injeção de dependência em Go. Tipos de interface não serão criados, uma vez que o objetivo principal aqui é o de exemplificar o uso de fx.  / cmd main. go / internal / app app. go module. go / controller controller. go module. go / usecase usecase. go

Como criar um ambiente aberto ao debate

Não é raro encontrar pessoas que sejam pressionadas a não expor uma opinião porque estão em um contexto em que cargos têm um maior peso na tomada de decisão que o conhecimento consistente. Não tenho dúvidas que todos confiamos que a um cargo está atrelada a experiência, ou seja, temos uma certa segurança e expectativa no papel que deve ser desempenhado. Porém, problemas podem surgir quando a pessoa errada está em um cargo importante. Um ambiente em que todos - de júnior a sênior - se sentem bem para participar é incapaz de se desenvolver sem o devido valor que merece a busca por conhecimento, capaz de estabelecer uma cultura em que as pessoas envolvidas se sentem convidadas ao debate inteligente, porque não está fundamento em posições, mas na colaboração por meio do saber. Equipes de desenvolvimento, líderes técnicos e de projetos, normalmente, precisam tomar decisões importantes para a vida de um serviço no longo prazo, cujos problemas atuais podem ser frutos de aspectos importantes q