Tratamento de erros é uma parte crucial de toda aplicação, seja ela grande ou pequena, e cada linguagem trata seus erros de formas diferentes. Em C
por exemplo, é bem comum tratar um erro retornando um código de erro em uma função, ja nas linguagens mais atuais como Java
e C#
os erros são chamados de exeções e são tratadas com blocos try...catch
assim como C++
e diversas outras linguagens de alto nível (python
tem um bloco semelhante mas que se chama try...except
).
Já Go
, na tentativa de deixar a vida do programador mais simples, se assemelha muito ao C
pois se aproveita da vantagem de se poder retornar mais de um valor em uma função e o conjunto defer, panic, recover
, como veremos mais tarde, tem um fluxo muito parecido com o do block try...catch
. Mesmo assim, tratamento de erros em Go é um tópico que gera muita discussão, geralmente reclamações sobre a quantidade o bloco
if err != nil {
return err
}
aparece no código. Isso geralmente ocorre porque um programador novo em golang acredita que existe um padrão "bala de prata" para tratar erros ou apenas pensa que substituindo todo bloco try-catch
pelo bloco acima resolverá seus problemas. Entretanto, como Rob Pike diz neste artigo, uma coisa fundamental que estes programadores esquecem é que:
Erros são valores!
Valores podem ser programados, e como erros são valores, erros podem ser programados.
Apesar de a comparação de um erro com nil
ser o tratamento mais óbvio possível, existem diversas outras formas de ser tratar um erro, e isso é algo que eu quero mostrar neste artigo, mas antes vamos nos aprofundar um pouco mais para entender como funciona o tipo error
em go.
O tipo error
nada mais é do que a interface abaixo
type error interface {
Error() string
}
e, assim como outros tipos interno de go, é pré-declarado no bloco universal, que engloba todo o código fonte de Go.
A implementação mais trival de um tipo error
é a implementação não exportada do pacote errors
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Ela pode ser construída a partir da função errors.New
:
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
A construção pela função acima, e também pela função fmt.Errorf()
, acabam se tornando uma dor de cabeça para quem está começando em go pois ao tentar realizar a comparação abaixo o resultado não é bem o esperado.
package main
import (
"errors"
"fmt"
)
func div(dividendo, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("Erro: divisão por 0")
}
return dividendo / divisor, nil
}
func main() {
_, err1 := div(10, 0)
_, err2 := div(10, 0)
fmt.Println(err1 == err2) //false
}
Isso ocorre porque, como pode ser visto na implementação da função errors.New
, essas funções retornam um ponteiro da interface e error, e quando a comparação é feita, ela é feita em cima do endereço dos ponteiros e não em seus valores. Uma forma de realizar essa comparação seria da seguinte forma:
func main() {
_, err1 := div(10, 0)
_, err2 := div(10, 0)
fmt.Println(err1.Error() == err2.Error()) //true
}
No exemplo acima a comparação está sendo feita entre as mensagens dos erros e não em cima dos seus endereços.
Dito isso, como poderiamos tratar melhor os nossos erros em go?
Para evitar o problema de comparaç~]ao de erros dito anteriormente, uma solução é a utilização de sentinelas que são apenas a declaração dos erros da aplicação em variáveis ou constantes. Tendo feito isso, a comparação não precisa mais ser entre o texto do error
, através a função Error()
, e pode ser feita através do endereço dele, pois agora o erro irá se encontrar no mesmo bloco de memória sempre.
package main
import (
"fmt"
"errors"
)
var ErroNomeVazio = errors.New("O nome informado está vazio.")
func DigaOla(nome string) (string, error) {
if len(nome) == 0 {
return "", ErroNomeVazio
}
return "Olá " + nome, nil
}
func main() {
if str, err := DigaOla(""); err == ErroNomeVazio {
//trata o erro aqui
}else{
fmt.Println(str)
}
}
Como pode ser visto no exemplo acima, a comparação do erro retornado não é mais com o texto do erro ou com nil
, mas sim com a variável ErroNomeVazio
.
Caso seja necessário tratar os erros de forma específica, uma alteranitiva é a criação de erros personalidados implementando a interface error
apresentada anteriormente. As vantagens desta abordagem é a criação de mensagens personalidadas e o tratamento do erro através da comparação do seu tipo.
// O exemplo completo você pode ver em https://github.com/luizvnasc/go-error-handling/tree/master/exemplos/3_erros_customizados
type StringVaziaError string
func (s StringVaziaError) Error() string {
return "A string está vazia."
}
type StringNumericaError string
func (s StringNumericaError) Error() string {
return "A string está " + string(s) + "contém apenas números."
}
type StringComCaracteresEspeciaisError string
func (s StringComCaracteresEspeciaisError) Error() string {
return "A string está " + string(s) + "contém apenas números."
}
func BemVindoCustom(nome string) (string, error) {
// verifica se a string é vazia
if s := strings.Trim(nome, " "); len(s) == 0 {
return "", StringVaziaError(nome)
}
// verifica se a string possui apenas números
if _, err := strconv.ParseFloat(nome, 64); err == nil {
return "", StringNumericaError(nome)
}
// verifica se a string possui caracteres especiais
if strings.ContainsAny(nome, `,.|!@#$%&*+_-=[]{};:/?\\'"()`) {
return "", StringComCaracteresEspeciaisError(nome)
}
return "Bem Vindo ao meetup da comunidade Golang CWB, " + nome + ".", nil
}
// DigaBemVindoCustom imprime uma mensagem de bem vindo para um participante do meetup.
func DigaBemVindoCustom(w io.Writer, nome string) {
msgBoasVindas, err := BemVindoCustom(nome)
if err != nil {
switch err.(type) {
case StringVaziaError:
fmt.Fprintln(w, "Não aceitamos pessoas anônimas!")
case StringNumericaError:
fmt.Fprintln(w, "Te entendo, somos todos apenas números.")
case StringComCaracteresEspeciaisError:
fmt.Fprintln(w, "Você ainda usa hotmail?")
}
}
fmt.Fprintln(w, msgBoasVindas)
}
No exemplo acima não precisamos da utilização de sentinelas para verificar o erro que foi retornado para realizarmos seu devido tratamento. Em vez disso comparamos o seu tipo com os tipos de erro da aplicação e, dependendo do erro retornado, é feita a impressão da mensagem de erro.
Até a versão o go 1.13, o pacote padrão errors
nãop possuía nenhuma implementação de stacktraces. Por este motivo foram criados algums pacotes de terceiros para solucionar este problema como palantir/stacktrace, go-erros/errors e pkg/errors, este último o mais popular entre eles com aproximadamente 5700 stars e 419 forks até a data de publicação deste artigo.
Modificando um pouco nosso exemplo anterior para utilizar a biblioteca pkg/errors
temos o seguinte código:
// O exemplo completo você pode visualizar em https://github.com/luizvnasc/go-error-handling/tree/master/exemplos/4_stacktrace
import (
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"github.com/pkg/errors"
)
// Os erros customizados a função que gera a mensagem de bem vindo não foram alterados.
// DigaBemVindo imprime uma mensagem de bem vindo para um participante do meetup.
func DigaBemVindo(w io.Writer, nome string) error {
msgBoasVindas, err := BemVindo(nome)
if err != nil {
return errors.Wrap(err, "Erro ao criar mensagem de boas vindas")
}
fmt.Fprintln(w, msgBoasVindas)
return nil
}
func main() {
nome := flag.String("nome", "folks", "Nome do participante do meetup")
flag.Parse()
err := DigaBemVindo(os.Stdout, *nome)
if err != nil {
log.Println(err)
switch errors.Cause(err).(type) {
case StringVaziaError:
fmt.Fprintln(os.Stdout, "Não aceitamos pessoas anônimas!")
case StringNumericaError:
fmt.Fprintln(os.Stdout, "Te entendo, somos todos apenas números.")
case StringComCaracteresEspeciaisError:
fmt.Fprintln(os.Stdout, "Você ainda usa hotmail?")
}
}
}
Como pode ser visto no exemplo, utilizamos duas funções da bibliteca pkg/errors
. A função Wrap(err error, message string)
embrulha um erro em um novo erro para que seja criada a stacktrace, já a função Cause(err error) error
percorre a pilha de forma recursiva até chegar a causa do problema, ou seja, aquele erro que embrulha nenhum outro erro.
O lançamento do go 1.13 trouxe algumas funcionalidades para os pacotes padrões errors
e fmt
para tratar erros que embrulham outros erros. Dentre elas a convenção de que um erro que embrulha outro deve implementar a função Unwrap que retorna o erro embrulhado, conforme o exemplo abaixo:
type AppError struct {
msg string
Err error
}
func (a *AppError) Unwrap() error { return a.Err }
Como pode ser visto, diferentemente do pacote pkg/errors
, nesta versão de go não foi implementada nenhuma função Cause
que percorreria toda a stacktrace até a raiz do erro.
Como dito anteriormente, o pacote fmt
ganhou uma nova funcionalidade para o embrulho de erros. Agora a função fmt.Errorf
suporta a expressão %w
que é responsável por embrulhar o erro informado dentro do erro criado. Modificando um pouco nosso exemplo de mensagem de boas vindas teremos:
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"errors"
)
type StringVaziaError string
var(
errNomeVazio = errors.New("O nome informado está vazio")
errNomeNumerico = errors.New("O nome informado é um número")
errNomeCaracterEspecial = errors.New("O nome informado contém caracteres especiais")
)
// BemVindo constrói uma menssagem de boas vindas desejada para um nome passado por parâmetro.
func BemVindo(nome string) (string, error) {
// verifica se a string é vazia
if s := strings.Trim(nome, " "); len(s) == 0 {
return "", errNomeVazio
}
// verifica se a string possui apenas números
if _, err := strconv.ParseFloat(nome, 64); err == nil {
return "", errNomeNumerico
}
// verifica se a string possui caracteres especiais
if strings.ContainsAny(nome, `,.|!@#$%&*+_-=[]{};:/?\\'"()`) {
return "", errNomeCaracterEspecial
}
return "Bem Vindo ao meetup da comunidade Golang CWB, " + nome + ".", nil
}
// DigaBemVindo imprime uma mensagem de bem vindo para um participante do meetup.
func DigaBemVindo(w io.Writer, nome string) error {
msgBoasVindas, err := BemVindo(nome)
if err != nil {
return fmt.Errorf("Erro ao criar mensagem de boas vindas: %w", err)
}
fmt.Fprintln(w, msgBoasVindas)
return nil
}
func main() {
nome := flag.String("nome", "folks", "Nome do participante do meetup")
flag.Parse()
err := DigaBemVindo(os.Stdout, *nome)
if err != nil {
log.Println(err)
if errors.Is(err, errNomeVazio){
fmt.Fprintln(os.Stdout, "Não aceitamos pessoas anônimas!")
}
if errors.Is(err, errNomeNumerico){
fmt.Fprintln(os.Stdout, "Te entendo, somos todos apenas números.")
}
if errors.Is(err, errNomeCaracterEspecial){
fmt.Fprintln(os.Stdout, "Você ainda usa hotmail?")
}
}
}
Veja que agora a função DigaBemVindo
embrulha o erro retornado pela função BemVindo
em um novo erro utilizando a função fmt.Errorf
. Também criamos sentinelas em vez de utilizar implementações customizadas. Isso foi apenas para podr mostrar outra funcionalidade implementada nesta versão do go que é a comparação de erros com a função Is
da biblioteca padrão errors
. Ela basicamente verifica se um erro, ou os erros contidos nele, são o mesmo que o sentinela.
Caso queira fazer uma comparação de tipo com dioferentes implementações de erro utilizamos a função As
,também da biblioteca padrão errors
. Vejamos como fica a implementação acima com utilizando a função errors.As
.
package main
import (
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
)
type StringVaziaError string
func (s StringVaziaError) Error() string {
return "A string está vazia."
}
type StringNumericaError string
func (s StringNumericaError) Error() string {
return "A string " + string(s) + " contém apenas números."
}
type StringComCaracteresEspeciaisError string
func (s StringComCaracteresEspeciaisError) Error() string {
return "A string " + string(s) + " contém apenas caracteres especiais."
}
var (
errNomeVazio StringVaziaError
errNomeNumerico StringNumericaError
errNomeCaracterEspecial StringComCaracteresEspeciaisError
)
// BemVindo constrói uma menssagem de boas vindas desejada para um nome passado por parâmetro.
func BemVindo(nome string) (string, error) {
// verifica se a string é vazia
if s := strings.Trim(nome, " "); len(s) == 0 {
return "", StringVaziaError(nome)
}
// verifica se a string possui apenas números
if _, err := strconv.ParseFloat(nome, 64); err == nil {
return "", StringNumericaError(nome)
}
// verifica se a string possui caracteres especiais
if strings.ContainsAny(nome, `,.|!@#$%&*+_-=[]{};:/?\\'"()`) {
return "", StringComCaracteresEspeciaisError(nome)
}
return "Bem Vindo ao meetup da comunidade Golang CWB, " + nome + ".", nil
}
// DigaBemVindo imprime uma mensagem de bem vindo para um participante do meetup.
func DigaBemVindo(w io.Writer, nome string) error {
msgBoasVindas, err := BemVindo(nome)
if err != nil {
return fmt.Errorf("Erro ao criar mensagem de boas vindas: %w", err)
}
fmt.Fprintln(w, msgBoasVindas)
return nil
}
func main() {
nome := flag.String("nome", "folks", "Nome do participante do meetup")
flag.Parse()
err := DigaBemVindo(os.Stdout, *nome)
if err != nil {
log.Println(err)
if errors.As(err, &errNomeVazio) {
fmt.Fprintln(os.Stdout, "Não aceitamos pessoas anônimas!")
}
if errors.As(err, &errNomeNumerico) {
fmt.Fprintln(os.Stdout, "Te entendo, somos todos apenas números.")
}
if errors.As(err, &errNomeCaracterEspecial) {
fmt.Fprintln(os.Stdout, "Você ainda usa hotmail?")
}
}
}
Veja que, mesmo utilizando tipos diferentes de erros, a função As
precisa de sentinelas para que haja a comparação, entretanto desta vez os sentinelas são tipos.
Outra forma de tratar erros em go é a utilização da declaração defer
e das funções panic e recover.
a declaração defer
empilha uma função na pilha de execução para ser executada ao fim da função na qual ela foi chamada. Essa declaração é extremamente quando se precisa fazer alguma limpeza no final da execução de alguma função.
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
A função acima faz a copia de um arquivo. Nela é possível ver que a declaração defer
é utilizada para fechar os arquivos de origem e destino. No caso do arquivo de origem, a declaração defer
garante que ele seja fechado mesmo que ocorra um erro durante a cópia. Outra vantagem da declaração defer
é na organização do código, pedir para fechar um arquivo que foi recém aberto é mais legível que lembrar de fechar ele no fim da função.
A função panic
para todo o fluxo de execução do go e entra em "pânico". Isso quer dizer que a se uma função F entra em pânico, ela irá para o restante da sua execução, a função declarada com defer
será executada normalmente, e irá retornar para quem à chamou. Isso ocorrerá até que todas as funções chamadas sejam retornadas e o programa quebre.
A função recover
recupera o programa em pânico e retorna a execução normal a partir dela. Ela deve ser utilizada dentro de uma declaração defer
, pois gantante que será executada em um programa em pânico.
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
No exemplo acima, a função f()
chama a função g(0)
que é executada recursivamente até o valor de i ser igual a 3. Após isso ela entra em pânico e retorna até ser recuperada na função f()
que pega o valor recuperado e imprime no terminal.
Considerar que o tratamento de erro em go é simplório é uma interpretação erronea pois, pesar da linguagem go não possuir exceções como outras linguagem, ela possui diversas formas de tratamento de erros que da um leque de possibilidades para o desenvolvedor. Analisar como os erros da sua aplicação devem ser tratados é um desafio e tanto e eu espero que este artigo tenha ajudado quem busca a melhor maneira de ultrapassá-lo.