O tratamento de erros em Golang

Métodos convencionais não semelhantes em outras linguagens de programação principais como JavaScript (que usa a instrução try… catch) ou Python (com o seu try… except bloco) para combater erros em Go requer uma abordagem diferente. Porquê? Porque suas características para lidar com erros são frequentemente mal aplicadas.

Neste post de blog, vamos dar uma olhada nas melhores práticas que poderiam ser usadas para lidar com erros em uma aplicação Go. Um entendimento básico de como Go funciona é tudo o que é necessário para digerir este artigo – se você se sentir preso em algum momento, não há problema em levar algum tempo e pesquisar conceitos desconhecidos.

O identificador em branco

O identificador em branco é um espaço reservado anônimo. Pode ser utilizado como qualquer outro identificador numa declaração, mas não introduz uma ligação. O identificador em branco fornece uma forma de ignorar valores à esquerda em uma atribuição e evitar erros de compilação sobre importações e variáveis não utilizadas em um programa. A prática de atribuir erros ao identificador em branco em vez de tratá-los corretamente não é segura, pois isso significa que você decidiu ignorar explicitamente o valor da função definida.

result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}

Sua razão para provavelmente fazer isso é que você não está esperando um erro da função (ou qualquer erro que possa ocorrer), mas isso pode criar efeitos em cascata no seu programa. A melhor coisa a fazer é lidar com um erro sempre que você puder.

Muitos erros de manipulação através de múltiplos valores de retorno

Uma maneira de lidar com erros é tirar vantagem do fato de que funções em Go suportam múltiplos valores de retorno. Assim você pode passar uma variável de erro ao lado do resultado da função que você está definindo:

func iterate(x, y int) (int, error) {}

No exemplo de código acima, temos que retornar a variável predefinida error se acharmos que há uma chance de nossa função falhar. error é um tipo de interface declarada no pacote Go’s built-in e seu valor zero é nil.

type error interface { Error() string }

Usualmente, retornar um erro significa que há um problema e retornar nil significa que não houve erros:

result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }

Assim sempre que a função iterate for chamada e err não for igual a nil, o erro retornado deve ser tratado apropriadamente – uma opção poderia ser criar uma instância de um mecanismo de nova tentativa ou limpeza. A única desvantagem com erros de manipulação desta forma é que não há aplicação do compilador Go, você tem que decidir como a função que você criou retorna o erro. É possível definir uma estrutura de erros e colocá-la na posição dos valores retornados. Uma maneira de fazer isso é usando o built-in errorString struct (você também pode encontrar esse código no código fonte do Go):

package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }

Na amostra de código acima, errorString embute um string que é retornado pelo método Error. Para criar um erro personalizado, você terá que definir sua estrutura de erros e usar conjuntos de métodos para associar uma função à sua struct:

// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}

O erro personalizado recém criado pode então ser reestruturado para usar o built-in error struct:

 import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}

Uma limitação do built-in error struct é que ele não vem com traços de pilha. Isto torna a localização onde um erro ocorreu muito difícil. O erro pode passar por uma série de funções antes de ser impresso. Para lidar com isso, você poderia instalar o pacote pkg/errors que fornece erros básicos de manuseio de primitivas como gravação de traços de pilha, envoltura de erro, desembrulhamento e formatação. Para instalar este pacote, execute este comando no seu terminal:

go get github.com/pkg/errors

Quando precisar de adicionar traços de pilha ou qualquer outra informação que facilite a depuração dos seus erros, use as funções New ou Errorf para fornecer erros que gravam o seu stack trace. Errorf implementa a interface fmt.Formatter que permite formatar os seus erros usando as runas de pacotes fmt (%s, %v, %+v etc):

import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}

Para imprimir traços de pilha em vez de uma mensagem de erro simples, tem de usar %+v em vez de %v no padrão de formato, e os traços de pilha serão semelhantes ao exemplo de código abaixo:

Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Defer, panic, and recover

Embora Go não tenha exceções, ele tem um mecanismo similar conhecido como “Defer, panic, and recover”. A ideologia de Go é que adicionar exceções como a declaração try/catch/finally em JavaScript resultaria em código complexo e encorajaria os programadores a rotularem muitos erros básicos, como falha na abertura de um arquivo, como excepcional. Você não deve usar defer/panic/recover como usaria throw/catch/finally; apenas em casos de falhas inesperadas e irrecuperáveis.

Defer é um mecanismo de linguagem que coloca sua chamada de função em uma pilha. Cada função diferida é executada em ordem inversa quando a função do host termina, independentemente de uma chamada de pânico ser ou não chamada. O mecanismo de diferimento é muito útil para limpar recursos:

package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}

Isto compilaria as:

If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!

Panic é uma função embutida que pára o fluxo normal de execução. Quando você chama panic em seu código, significa que você decidiu que seu chamador não pode resolver o problema. Assim panic só deve ser usado em casos raros onde não é seguro que o seu código ou qualquer um que integre o seu código continue nesse ponto. Aqui está um exemplo de código representando como panic funciona:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

O exemplo acima compilaria como:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.

Como mostrado acima, quando panic é usado e não manipulado, o fluxo de execução pára, todas as funções diferidas são executadas em ordem inversa e os traços da pilha são impressos.

Você pode usar a função recover embutida para manipular panic e retornar os valores que passam de uma chamada de pânico. recover deve ser sempre chamada em uma função defer senão ela retornará nil:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Como pode ser visto na amostra de código acima, recover impede que todo o fluxo de execução seja interrompido porque nós lançamos uma função panic e o compilador retornaria:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.

Para reportar um erro como um valor de retorno, você tem que chamar a função recover no mesmo goroutine que a função panic é chamada, recuperar uma estrutura de erro da função recover, e passá-la para uma variável:

package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}

Todas as funções diferidas serão executadas após uma chamada de função, mas antes de uma declaração de retorno. Então, você pode definir uma variável retornada antes que um comando de retorno seja executado. O exemplo de código acima compilaria como:

If it's more than 100 degrees...Then there's nothing we can doProgram exited.

Error wrapping

O erro anterior de wrapping em Go só era acessível através de pacotes como pkg/errors. No entanto, com a última versão do Go – versão 1.13, o suporte para o empacotamento de erros está presente. De acordo com as notas de lançamento:

Um erro e pode embrulhar outro erro w fornecendo um método Unwrap que retorna w. Ambos e e w estão disponíveis para os programas, permitindo que e forneça contexto adicional a w ou reinterprete-o enquanto ainda permite que os programas tomem decisões baseadas em w.

Para criar erros embrulhados, fmt.Errorf agora tem um verbo %w e para inspecionar e desembrulhar erros, um par de funções foram adicionadas ao pacote error:

errors.Unwrap: Esta função basicamente inspeciona e expõe os erros subjacentes em um programa. Ela retorna o resultado de chamar o método Unwrap em Err. Se o tipo de erro contém um método Unwrap devolvendo um erro. Caso contrário, Unwrap retorna nil.

package errorstype Wrapper interface{ Unwrap() error}

Below é um exemplo de implementação do método Unwrap:

func(e*PathError)Unwrap()error{ return e.Err}

errors.Is: Com esta função, você pode comparar um valor de erro com o valor da sentinela. O que torna esta função diferente das nossas verificações de erro usuais é que em vez de comparar o valor da sentinela com um erro, ela o compara com todos os erros da cadeia de erros. Ela também implementa um método Is sobre um erro para que um erro possa se colocar como uma sentinela mesmo que não seja um valor sentinela.

func Is(err, target error) bool

Na implementação básica acima, Is verifica e relata se err ou qualquer um dos errors em sua cadeia são iguais ao alvo (valor sentinela).

errors.As: Esta função fornece uma maneira de lançar para um tipo de erro específico. Ela procura o primeiro erro na cadeia de erros que corresponde ao valor da sentinela e se encontrado, define o valor da sentinela para esse valor de erro e retorna true:

package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}

Você pode encontrar esse código no código fonte de Go.

Resultado do compilador:

Failed at path: non-existingProgram exited.

Um erro corresponde ao valor da sentinela se o valor concreto do erro for atribuível ao valor apontado pelo valor da sentinela. As entrará em pânico se o valor da sentinela não for um ponteiro non-nil a um tipo que implementa o erro ou a qualquer tipo de interface. As retorna falso se err for nil.

Resumo

A comunidade Go tem feito avanços impressionantes desde o início com suporte a vários conceitos de programação e introduzindo maneiras ainda mais concisas e fáceis de lidar com erros. Você tem alguma idéia de como lidar ou trabalhar com erros que possam aparecer no seu programa Go? Avise-me nos comentários abaixo.

Recursos:
A especificação da linguagem de programação Go em Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes

LogRocket: Visibilidade total em seus aplicativos web

LogRocket é uma solução de monitoramento de aplicativos frontend que permite que você reproduza problemas como se eles tivessem acontecido no seu próprio navegador. Ao invés de adivinhar por que os erros acontecem, ou pedir aos usuários por screenshots e log dumps, o LogRocket permite que você reproduza novamente a sessão para entender rapidamente o que deu errado. Ele funciona perfeitamente com qualquer aplicativo, independentemente do framework, e tem plugins para registrar contexto adicional do Redux, Vuex e @ngrx/store.

Além de registrar ações e estado do Redux, o LogRocket registra os logs do console, erros de JavaScript, stacktraces, solicitações/respostas de rede com cabeçalhos + corpos, metadados do navegador, e logs personalizados. Ele também utiliza o DOM para gravar o HTML e CSS na página, recriando vídeos pixel-perfect mesmo dos mais complexos aplicativos de uma página.

Experimente gratuitamente.

Deixe uma resposta

O seu endereço de email não será publicado.