Tratamento de Erros
Em Go, existem três níveis de exceções:
error: erro de fluxo normal, precisa ser tratado. Se ignorado e não tratado, o programa não travarápanic: problema muito sério, o programa deve sair imediatamente após tratar o problemafatal: problema extremamente fatal, o programa deve sair imediatamente
Precisamente falando, a linguagem Go não tem exceções. Ela expressa erros através de erros. Da mesma forma, Go também não tem instruções try-catch-finally. Os criadores de Go esperavam tornar os erros controláveis. Eles não queriam que fosse necessário aninhar vários try-catch para fazer qualquer coisa, então na maioria dos casos os erros são retornados como valores de retorno de funções, como no exemplo abaixo:
func main() {
// Abre um arquivo
if file, err := os.Open("README.txt"); err != nil {
fmt.Println(err)
return
}
fmt.Println(file.Name())
}A intenção deste código é clara: abrir um arquivo chamado README.txt. Se a abertura falhar, a função retornará um erro e imprimirá a mensagem de erro. Se o erro for nil, significa que a abertura foi bem-sucedida e o nome do arquivo será impresso.
Parece ser mais conciso que try-catch, mas se houver muitas chamadas de função, haverá instruções de julgamento if err != nil em todos os lugares. Por exemplo, no exemplo abaixo, que é um demo de cálculo de hash de arquivo, a instrução if err != nil aparece três vezes neste pequeno trecho de código.
func main() {
sum, err := checksum("main.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(sum)
}
func checksum(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
_, err = io.Copy(hash, file)
if err != nil {
return "", err
}
var hexSum [64]byte
sum := hash.Sum(nil)
hex.Encode(hexSum[:], sum)
return string(hexSum[:]), nil
}Por isso, o ponto mais criticado em Go é o tratamento de erros. No código fonte de Go, if err != nil ocupa uma parte considerável. Rust também retorna valores de erro, mas ninguém critica isso, porque resolveu esse tipo de problema através de açúcar sintático. Comparado com Rust, o açúcar sintático de Go não pode ser considerado muito, pode-se dizer que quase não existe.
No entanto, devemos ver as coisas de forma dialética. Tudo tem seus prós e contras. As vantagens do tratamento de erros em Go são:
- Baixa carga mental: se há erro, trate; se não tratar, retorne
- Legibilidade: como a forma de tratamento é muito simples, na maioria dos casos é muito fácil ler o código
- Fácil de depurar: cada erro é produzido através do valor de retorno da chamada de função, pode-se encontrar camada por camada, raramente aparece um erro repentinamente sem saber de onde veio
Mas também há muitas desvantagens:
- Não há informação de pilha nos erros (precisa de pacotes de terceiros para resolver ou encapsular por conta própria)
- Feio, muito código repetitivo (depende da preferência pessoal)
- Erros personalizados são declarados através de
var, é uma variável e não uma constante (realmente não deveria ser) - Problema de sombreamento de variáveis
As propostas e discussões da comunidade sobre o tratamento de erros em Go nunca pararam desde o nascimento de Go. Há uma piada: se você consegue aceitar o tratamento de erros de Go, então você é um Gopher qualificado.
TIP
Aqui estão dois artigos da equipe Go sobre tratamento de erros, se interessar pode dar uma olhada
error
error é um tipo de erro de fluxo normal, sua aparição pode ser aceita. Na maioria dos casos deve ser tratado, mas também pode ser ignorado. O nível de severidade do error não é suficiente para parar toda a execução do programa. error em si é uma interface pré-definida, esta interface tem apenas um método Error(), o valor de retorno deste método é string, usado para imprimir a mensagem de erro.
type error interface {
Error() string
}error também teve grandes mudanças na história. Na versão 1.13, a equipe Go lançou erros encadeados e forneceu um mecanismo de verificação de erros mais completo, que será apresentado a seguir.
Criação
Existem várias formas de criar um error. A primeira é usar a função New do pacote errors.
err := errors.New("este é um erro")A segunda é usar a função Errorf do pacote fmt, que pode obter um error com parâmetros formatados.
err := fmt.Errorf("este é o erro do %dº parâmetro formatado", 1)Abaixo está um exemplo completo
func sumPositive(i, j int) (int, error) {
if i <= 0 || j <= 0 {
return -1, errors.New("devem ser inteiros positivos")
}
return i + j, nil
}Na maioria dos casos, para melhor manutenção, geralmente não se cria error temporariamente, mas se usa os errors comuns como variáveis globais. Por exemplo, o código abaixo extraído do arquivo os\erros.go
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)Pode-se ver que todos são variáveis definidas com var
Erro Personalizado
Através da implementação do método Error(), pode-se facilmente personalizar error. Por exemplo, errorString do pacote errors é uma implementação muito simples.
func New(text string) error {
return &errorString{text}
}
// struct errorString
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}Como a implementação de errorString é simples demais e tem capacidade de expressão insuficiente, muitas bibliotecas open source incluindo a biblioteca oficial escolhem personalizar error para satisfazer diferentes necessidades de erro.
Passagem
Em algumas situações, o chamador chama uma função que retorna um erro, mas o chamador em si não é responsável por tratar o erro, então também retorna o erro como valor de retorno, jogando para o chamador do nível superior. Este processo é chamado de passagem. Durante a passagem, o erro pode ser encapsulado em várias camadas. Quando o chamador do nível superior quer julgar o tipo de erro para fazer tratamentos diferentes, pode não conseguir identificar a categoria do erro ou fazer julgamento errado. Os erros encadeados foram criados exatamente para resolver essa situação.
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}wrappError também implementa a interface error, e também tem um método adicional Unwrap, usado para retornar a referência interna ao error original. Com encapsulamento em camadas, forma-se uma lista encadeada de erros. Seguindo a lista encadeada para cima, é muito fácil encontrar o erro original. Como esta struct não é exposta externamente, só se pode usar a função fmt.Errorf para criar, por exemplo
err := errors.New("este é um erro original")
wrapErr := fmt.Errorf("erro, %w", err)Ao usar, deve-se usar o verbo de formato %w, e o parâmetro só pode ser um error válido.
Tratamento
O último passo no tratamento de erros é como tratar e verificar erros. O pacote errors fornece várias funções convenientes para tratar erros.
func Unwrap(err error) errorA função errors.Unwrap() é usada para desencapsular uma cadeia de erros. Sua implementação interna também é muito simples
func Unwrap(err error) error {
u, ok := err.(interface { // Asserção de tipo, se implementa o método
Unwrap() error
})
if !ok { // Se não implementa, significa que é um error básico
return nil
}
return u.Unwrap() // Caso contrário, chama Unwrap
}Após desencapsular, retorna o erro encapsulado na cadeia de erros atual. O erro encapsulado ainda pode ser uma cadeia de erros. Se quiser encontrar o valor ou tipo correspondente na cadeia de erros, pode-se fazer busca recursiva e correspondência, mas a biblioteca padrão já fornece funções similares.
func Is(err, target error) boolA função errors.Is serve para julgar se a cadeia de erros contém o erro especificado. Exemplo abaixo
var originalErr = errors.New("este é um erro")
func wrap1() error { // Encapsula o erro original
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // Erro original
return originalErr
}
func main() {
err := wrap1()
if errors.Is(err, originalErr) { // Se usar if err == originalErr será false
fmt.Println("original")
}
}Portanto, ao julgar erros, não se deve usar o operador ==, mas sim errors.Is().
func As(err error, target any) boolA função errors.As() serve para encontrar o primeiro erro com tipo correspondente na cadeia de erros e atribuir o valor ao err passado. Em alguns casos, precisa-se converter o erro do tipo error para um tipo de implementação de erro específico para obter detalhes de erro mais específicos. Usar asserção de tipo em uma cadeia de erros é inválido, porque o erro original está encapsulado em uma struct, e é por isso que a função As é necessária. Exemplo abaixo
type TimeError struct { // error personalizado
Msg string
Time time.Time // Registra o momento em que o erro ocorreu
}
func (m TimeError) Error() string {
return m.Msg
}
func NewMyError(msg string) error {
return &TimeError{
Msg: msg,
Time: time.Now(),
}
}
func wrap1() error { // Encapsula o erro original
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // Erro original
return NewMyError("original error")
}
func main() {
var myerr *TimeError
err := wrap1()
// Verifica se há erro do tipo *TimeError na cadeia de erros
if errors.As(err, &myerr) { // Imprime o tempo do TimeError
fmt.Println("original", myerr.Time)
}
}target deve ser um ponteiro para error. Como na criação da struct é retornado um ponteiro de struct, error na verdade é do tipo *TimeError, então target deve ser do tipo **TimeError.
No entanto, o pacote errors fornecido oficialmente não é suficiente, porque não tem informação de pilha e não pode localizar. Geralmente é recomendado usar outro pacote de extensão oficial
github.com/pkg/errorsExemplo
import (
"fmt"
"github.com/pkg/errors"
)
func Do() error {
return errors.New("error")
}
func main() {
if err := Do(); err != nil {
fmt.Printf("%+v", err)
}
}Saída
some unexpected error happened
main.Do
D:/WorkSpace/Code/GoLeran/golearn/main.go:9
main.main
D:/WorkSpace/Code/GoLeran/golearn/main.go:13
runtime.main
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
runtime.goexit
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650Através da saída formatada, pode-se ver a informação de pilha. Por padrão, a pilha não é impressa. Este pacote é essencialmente uma versão extendida do pacote errors da biblioteca padrão, ambos são escritos oficialmente, não se sabe por que não foi incorporado à biblioteca padrão.
panic
panic traduzido para o chinês significa pânico, indica um problema de programa muito sério. O programa precisa parar imediatamente para tratar o problema, caso contrário o programa para de executar imediatamente e imprime a informação de pilha. panic é a forma de expressão de exceção em tempo de execução em Go, geralmente aparece em operações perigosas, principalmente para limitar as perdas a tempo e evitar consequências mais graves. No entanto, panic fará o trabalho de limpeza do programa antes de sair, e panic também pode ser recuperado para garantir que o programa continue executando.
Abaixo está um exemplo de escrever valor em um map nil, certamente vai disparar panic
func main() {
var dic map[string]int
dic["a"] = 'a'
}panic: assignment to entry in nil mapTIP
Quando existem múltiplas goroutines no programa, se qualquer goroutine disparar panic e não for capturado, todo o programa vai travar
Criação
Criar panic explicitamente é muito simples, basta usar a função embutida panic. A assinatura da função é
func panic(v any)A função panic recebe um parâmetro v do tipo any. Ao imprimir a informação de pilha de erro, v também será impresso. Exemplo de uso abaixo
func main() {
initDataBase("", 0)
}
func initDataBase(host string, port int) {
if len(host) == 0 || port == 0 {
panic("parâmetros de conexão de dados inválidos")
}
// ...outra lógica
}Quando a inicialização da conexão do banco de dados falha, o programa não deve iniciar, porque sem banco de dados o programa não tem sentido para executar, então aqui deve disparar panic
panic: parâmetros de conexão de dados inválidosLimpeza
Antes de sair por causa de panic, o programa fará algum trabalho de limpeza, como executar instruções defer.
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("panic")
defer fmt.Println("D")
}A saída é
C
B
A
panic: panicE as instruções defer das funções upstream também serão executadas. Exemplo abaixo
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panic")
defer fmt.Println(3)
}Saída
C
2
1
B
A
panic: panicdefer também pode aninhar panic. Abaixo está um exemplo mais complexo
func main() {
defer fmt.Println("A")
defer func() {
func() {
panic("panicA")
defer fmt.Println("E")
}()
}()
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}A ordem de execução do panic aninhado em defer permanece a mesma. Quando ocorre panic, a lógica subsequente não poderá ser executada.
C
2
1
A
panic: panicB
panic: panicAEm resumo, quando ocorre panic, a função onde está sai imediatamente, executa o trabalho de limpeza da função atual, como defer, depois joga para cima camada por camada. As funções upstream também fazem trabalho de limpeza até o programa parar de executar.
Quando uma goroutine filha dispara panic, o trabalho de limpeza da goroutine atual não é disparado. Se até a goroutine filha sair não houver recuperação do panic, o programa vai parar de executar diretamente.
var waitGroup sync.WaitGroup
func main() {
demo()
}
func demo() {
waitGroup.Add(1)
defer func() {
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
waitGroup.Wait() // Goroutine pai bloqueia esperando goroutine filha terminar
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
waitGroup.Done()
}A saída é
C
2
1
panic: panicBPode-se ver que nenhuma das instruções defer em demo() foi executada, o programa saiu diretamente. Note que se não houver waitGroup para bloquear a goroutine pai, a velocidade de execução de demo() pode ser mais rápida que a velocidade de execução da goroutine filha, e o resultado da saída se tornará muito enganoso. Abaixo, modificando ligeiramente o código
func main() {
demo()
}
func demo() {
defer func() {
// Trabalho de limpeza da goroutine pai leva 20ms
time.Sleep(time.Millisecond * 20)
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
// Goroutine filha precisa executar alguma lógica, leva 1ms
time.Sleep(time.Millisecond)
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}A saída é
C
D
2
1
panic: panicBNeste exemplo, quando a goroutine filha dispara panic, a goroutine pai já completou a execução da função e entrou no trabalho de limpeza. Ao executar o último defer, por coincidência encontrou a goroutine filha disparando panic, então o programa saiu diretamente.
Recuperação
Quando ocorre panic, usar a função embutida recover() pode tratar a tempo e garantir que o programa continue executando. Deve ser executado em uma instrução defer. Exemplo de uso abaixo.
func main() {
dangerOp()
fmt.Println("programa saiu normalmente")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperado")
}
}()
panic("ocorreu panic")
}O chamador não sabe nada sobre o panic ocorrido dentro da função dangerOp(). O programa executa a lógica restante e sai normalmente, então a saída é
ocorreu panic
panic recuperado
programa saiu normalmenteMas na verdade o uso de recover() tem muitas armadilhas implícitas. Por exemplo, usar recover novamente em closure dentro de defer.
func main() {
dangerOp()
fmt.Println("programa saiu normalmente")
}
func dangerOp() {
defer func() {
func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperado")
}
}()
}()
panic("ocorreu panic")
}A função closure pode ser vista como chamando uma função. panic é passado para cima e não para baixo, naturalmente a função closure não consegue recuperar o panic, então a saída é
panic: ocorreu panicAlém disso, há uma situação muito extrema, que é quando o parâmetro de panic() é nil.
func main() {
dangerOp()
fmt.Println("programa saiu normalmente")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperado")
}
}()
panic(nil)
}Neste caso, panic realmente será recuperado, mas não imprimirá nenhuma mensagem de erro.
Saída
programa saiu normalmenteEm resumo, a função recover tem alguns pontos de atenção
- Deve ser usada em
defer - Mesmo usada múltiplas vezes, apenas uma conseguirá recuperar o
panic recoverem closure não recuperará nenhumpanicda função externa- O parâmetro de
panicnão pode sernil
fatal
fatal é um tipo de problema extremamente sério. Quando ocorre fatal, o programa precisa parar de executar imediatamente, não executará nenhum trabalho de limpeza. Geralmente é chamando a função Exit do pacote os para sair do programa, como mostrado abaixo
func main() {
dangerOp("")
}
func dangerOp(str string) {
if len(str) == 0 {
fmt.Println("fatal")
os.Exit(1)
}
fmt.Println("lógica normal")
}Saída
fatalProblemas de nível fatal geralmente raramente são disparados explicitamente, na maioria dos casos são disparados passivamente.
