Pular para o conteúdo principal

Errors

9 min de leitura•Arquivado emLinguagem de Programação Goem

Aprenda como Go trata errors explicitamente por meio de valores de retorno, como criar e encapsular errors, e como compará-los com errors.Is e errors.As.

Na maioria das linguagens, errors interrompem o fluxo normal de execução. Uma exceção é lançada, a pilha se desfaz e um bloco catch em algum lugar trata o problema — muitas vezes longe de onde a falha ocorreu de fato. Go adota uma abordagem fundamentalmente diferente: errors são valores comuns, retornados e verificados como qualquer outro resultado. Não existe throw, nem catch, nem propagação implícita. Quando uma função pode falhar, ela declara isso na sua assinatura, e quem chama decide o que fazer.

A error interface

O type error é uma interface pré-declarada com um único método:

Qualquer type que implemente Error() string satisfaz error. Os types de error da standard library não têm nada de especial — eles implementam a mesma interface que você pode implementar, o que torna os errors em Go composáveis e extensíveis por design.

Criando errors

O package errors oferece a forma mais simples de criar um error a partir de uma string fixa:

Quando a mensagem precisa incluir valores em tempo de execução, use fmt.Errorf:

Ambos retornam um valor que satisfaz error. Nos casos em que quem chama precisa inspecionar os campos do error — não apenas ler sua mensagem — defina um type customizado:

*ValidationError satisfaz error porque implementa Error() string. Quem só precisa da mensagem trata esse error como qualquer outro; quem precisa dos dados estruturados pode extraí-los com errors.As, abordado mais adiante.

Custom error types se tornam mais poderosos quando carregam contexto estruturado e encapsulam uma causa subjacente:

*QueryError satisfaz error e também participa de cadeias de error: o método Unwrap expõe a causa subjacente para que errors.Is e errors.As possam atravessá-la. Quem só precisa da mensagem vê "insert on users: duplicate key"; quem precisa reagir a campos específicos — por exemplo, para logar a operação e o nome da tabela, ou para fazer retry de um tipo específico de query — pode extrair a struct com errors.As.

Tratando errors

Por convenção, uma função que pode falhar retorna error como seu último valor de retorno:

Quem chama verifica o error antes de usar o resultado:

Esse padrão if err != nil é idiomático em Go. O error é tratado imediatamente no local da chamada, não adiado para um bloco de tratamento em outro lugar. Os errors se propagam pela pilha de chamadas uma verificação explícita por vez, o que mantém o caminho de falha tão legível quanto o caminho de sucesso. Múltiplos blocos try-catch aninhados obscurecem quais operações podem falhar e como elas se relacionam; retornos explícitos tornam cada falha visível.

Duas convenções regem as strings de error: não começam com letra maiúscula e não terminam com pontuação. Errors são frequentemente compostos em mensagens maiores, e essas convenções mantêm o resultado legível:

Sentinel errors

Um sentinel error é uma variável exportada de nível de package que representa uma condição específica e identificável:

Uma função retorna o sentinel quando essa condição ocorre:

Quem chama compara o error retornado com o sentinel para distinguir esse caso dos demais:

A standard library usa esse padrão amplamente: io.EOF sinaliza o fim de um stream de dados; sql.ErrNoRows sinaliza que uma query não retornou linhas. Sentinel errors permitem que o código reaja a condições específicas sem precisar fazer parse de strings de error.

Wrapping errors

Ao propagar um error pela pilha de chamadas, muitas vezes é necessário adicionar contexto. Sem wrapping, o error original viaja sem alterações e o caminho de chamada fica invisível:

fmt.Errorf com o verbo %w encapsula o error original enquanto adiciona uma mensagem:

O error encapsulado é preservado em uma error chain — a série de errors vinculados pelo wrapping. O error original permanece acessível através da chain; quem chama pode inspecioná-lo com errors.Is e errors.As, abordados abaixo.

Usar %v em vez de %w inclui a mensagem original na string formatada, mas quebra a chain:

Para combinar múltiplos errors independentes em um único valor, use errors.Join:

errors.Join descarta errors nil. Se todos os errors fornecidos forem nil, retorna nil.

Unwrapping errors

errors.Unwrap recupera o próximo error na chain:

errors.Unwrap chama o método Unwrap() error no valor, se ele existir. Errors criados por fmt.Errorf com %w implementam esse método automaticamente. Quando o error não tem mais chain, errors.Unwrap retorna nil.

Errors criados por errors.Join implementam Unwrap() []error em vez disso, retornando todos os errors encapsulados como um slice. errors.Unwrap não lida com essa forma — retorna nil para eles.

Comparando errors

Comparar errors com == só funciona de forma confiável para sentinels. Uma vez que um error é encapsulado, == não encontra o original — o wrapper é um valor diferente. errors.Is e errors.As percorrem a chain inteira.

errors.Is

errors.Is(err, target) retorna true se qualquer error na chain corresponder a target:

A comparação padrão é ==. Um type de error pode substituí-la implementando um método Is(target error) bool. Quando presente, errors.Is o chama em vez de usar ==:

Com esse método, dois valores *NotFoundError com o mesmo ID são considerados iguais por errors.Is, independentemente da identidade do pointer:

errors.As

errors.As(err, target) percorre a chain e, se encontrar um error cujo type é atribuível ao type apontado por target, define target e retorna true. É a forma padrão de extrair dados estruturados de custom error types.

Usando o type QueryError definido anteriormente:

errors.As faz o unwrap pela chain automaticamente — aqui ele encontra *QueryError dentro do wrapper do fmt.Errorf sem que você precise fazer o unwrap manualmente.

O mesmo padrão funciona com qualquer struct customizada:

target deve ser um pointer não-nil para um type que implemente error, ou para qualquer type de interface. Um type de error pode customizar o comportamento de match com um método As(target any) bool. Isso é particularmente útil quando você quer que errors.As encontre uma interface em vez de um concrete type:

Quem chama pode então extrair a interface Retryable da chain sem conhecer o concrete type: