Tratamento de erros
Nesta aula, você aprenderá sobre o tratamento de erros em Go, incluindo o tipo error, o padrão if err != nil, como criar erros com errors.New e fmt.Errorf, e as práticas idiomáticas da linguagem. O conteúdo cobre desde conceitos básicos até boas práticas para escrever código robusto e claro.
Em Go, o tratamento de erros é explícito e integrado à linguagem de forma simples, mas poderosa. Diferente de linguagens que usam exceções, Go adota uma abordagem onde os erros são valores comuns que podem ser retornados e verificados. Isso incentiva o programador a lidar com cada erro de forma consciente, resultando em código mais previsível e fácil de depurar.
Nesta aula, vamos explorar o tipo error, a construção if err != nil, como criar seus próprios erros com errors.New e fmt.Errorf, e as convenções idiomáticas que tornam o código Go elegante e eficiente.
O tipo error
Em Go, o tipo error é uma interface pré-definida com um único método: Error() string. Qualquer tipo que implemente esse método satisfaz a interface error e pode ser usado como um valor de erro.
A definição da interface é:
type error interface {
Error() string
}Isso significa que qualquer valor que tenha um método Error() string pode ser tratado como um erro. Na prática, a biblioteca padrão fornece implementações prontas, mas você também pode criar seus próprios tipos de erro para fornecer mais contexto.
Exemplo de uso do tipo error:
package main
import "fmt"
func main() {
var err error = fmt.Errorf("algo deu errado")
fmt.Println(err) // chama o método Error()
}Saída:
algo deu erradoif err != nil
O padrão mais comum em Go para tratar erros é verificar se o valor retornado por uma função é nil ou não. A maioria das funções que podem falhar retorna um error como último valor de retorno. Se o erro for nil, a operação foi bem-sucedida; caso contrário, o erro deve ser tratado.
Exemplo típico:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("arquivo.txt")
if err != nil {
fmt.Println("Erro ao abrir arquivo:", err)
return
}
defer f.Close()
// ... usa o arquivo
}Não há exceções ou blocos try/catch. O fluxo de erro é explícito e fácil de seguir. É uma boa prática tratar o erro imediatamente após a chamada da função, em vez de adiá-lo. Isso torna o código mais legível e evita que erros passem despercebidos.
Às vezes, você pode querer ignorar um erro intencionalmente, mas isso deve ser raro. Use _ para descartar o valor do erro:
result, _ := algumaFuncao() // não recomendadoEvite essa prática, pois ela pode esconder problemas.
Criando erros (errors.New, fmt.Errorf)
A biblioteca padrão oferece duas formas principais de criar erros simples: errors.New e fmt.Errorf.
errors.New recebe uma string e retorna um error com essa mensagem. É útil para erros estáticos.
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("ocorreu um erro")
fmt.Println(err)
}Saída:
ocorreu um errofmt.Errorf permite formatar a mensagem de erro usando verbos de formatação, similar ao fmt.Sprintf. É útil para incluir valores dinâmicos na mensagem.
package main
import (
"fmt"
)
func main() {
nome := "João"
idade := 30
err := fmt.Errorf("usuário %s tem idade %d, não permitido", nome, idade)
fmt.Println(err)
}Saída:
usuário João tem idade 30, não permitidoPara erros mais complexos, você pode criar tipos personalizados que implementam a interface error. Isso permite adicionar informações extras, como códigos de erro ou campos estruturados.
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("erro %d: %s", e.Code, e.Message)
}
func main() {
err := &MyError{Code: 404, Message: "recurso não encontrado"}
fmt.Println(err)
}Saída:
erro 404: recurso não encontradoIdiomático
Go tem convenções claras para tratamento de erros que são consideradas idiomáticas. Aqui estão as principais práticas:
- Verifique erros imediatamente: Após uma chamada de função que retorna erro, verifique-o logo em seguida. Não acumule erros para depois.
- Use múltiplos valores de retorno: Funções que podem falhar geralmente retornam o resultado e um erro. Isso é mais explícito do que lançar exceções.
- Prefira sentinelas a booleanos: Em vez de retornar um booleano indicando sucesso, retorne um erro. Isso fornece mais contexto.
- Não abuse de panic/recover:
panicé para erros irrecuperáveis, como falhas de programação. Use erros para situações esperadas. - Encadeie erros com %w: A partir do Go 1.13, você pode usar
%wemfmt.Errorfpara criar uma cadeia de erros, permitindo que o erro original seja desempacotado comerrors.Isouerrors.As.
Exemplo de encadeamento:
package main
import (
"errors"
"fmt"
)
func foo() error {
return fmt.Errorf("foo: %w", errors.New("erro interno"))
}
func main() {
err := foo()
if errors.Is(err, errors.New("erro interno")) {
fmt.Println("erro interno detectado")
}
fmt.Println(err)
}Saída:
foo: erro internoOutra prática idiomática é usar nomes de variáveis como err para erros. Evite nomes como error ou erro.
Por fim, lembre-se de que erros são valores: você pode armazená-los, compará-los e passá-los por funções. Trate-os com a mesma seriedade que qualquer outro dado.
Boas práticas e observações finais
Algumas dicas para escrever código Go robusto:
- Sempre documente os erros que sua função pode retornar, especialmente se forem de tipos personalizados.
- Use
deferpara garantir que recursos sejam liberados, mesmo em caso de erro. - Evite repetir mensagens de erro; crie erros reutilizáveis quando apropriado.
- Em pacotes públicos, considere exportar tipos de erro para que os usuários possam fazer type assertion ou switch.
- Teste cenários de erro tanto quanto cenários de sucesso.
O tratamento de erros em Go é uma das suas características mais distintivas. Dominá-lo é essencial para escrever código confiável e de fácil manutenção.
Referências
- Go Blog: Errors are values
- Go Blog: Working with Errors in Go 1.13
- Package errors - Go Documentation
- Package fmt - Errorf - Go Documentation
- Effective Go: Errors
Exercícios
Crie uma função que divide dois números float64 e retorna o resultado e um erro. Se o divisor for zero, retorne um erro com a mensagem "divisão por zero".
✓ Resposta:func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("divisão por zero") } return a / b, nil }Escreva um programa que tenta abrir um arquivo chamado "config.json" e, se houver erro, imprime "Erro: [mensagem do erro]" e encerra. Use if err != nil.
✓ Resposta:package main import ( "fmt" "os" ) func main() { _, err := os.Open("config.json") if err != nil { fmt.Println("Erro:", err) return } fmt.Println("Arquivo aberto com sucesso") }Crie um tipo de erro personalizado chamado
ValidationErrorque tenha um campoField(string) eValue(interface{}). Implemente o métodoError() stringpara retornar "campo [Field] inválido: [Value]".✓ Resposta:type ValidationError struct { Field string Value interface{} } func (e *ValidationError) Error() string { return fmt.Sprintf("campo %s inválido: %v", e.Field, e.Value) }Usando
fmt.Errorfcom%w, escreva uma função que chama outra função que retorna um erro e o encapsula com uma mensagem adicional. Depois, useerrors.Ispara verificar se o erro original está presente.✓ Resposta:package main import ( "errors" "fmt" ) func inner() error { return errors.New("erro interno") } func outer() error { err := inner() if err != nil { return fmt.Errorf("outer: %w", err) } return nil } func main() { err := outer() if errors.Is(err, errors.New("erro interno")) { fmt.Println("erro interno encontrado") } }Escreva uma função que recebe uma string e retorna um erro se a string estiver vazia. Use
errors.Newpara criar o erro. Em seguida, chame a função em main e trate o erro de forma idiomática.✓ Resposta:package main import ( "errors" "fmt" ) func validate(s string) error { if s == "" { return errors.New("string vazia") } return nil } func main() { err := validate("") if err != nil { fmt.Println("Erro:", err) return } fmt.Println("String válida") }