Não sem pânico no Go

Olá, queridos leitores do Habrahabr. Ao discutir um possível novo design para o tratamento de erros e debater sobre os benefícios do tratamento explícito de erros, proponho considerar alguns recursos de erros, pânico e sua recuperação no Go que serão úteis na prática.
imagem


erro


O erro é uma interface. E, como a maioria das interfaces no Go, a definição de erro é curta e simples:


type error interface { Error() string } 

Acontece que qualquer tipo que tenha o método Error pode ser usado como erro. Como Rob Pike ensinou, Erros são valores e podem ser usados ​​para manipular e programar várias lógicas.


Existem duas funções na biblioteca padrão Go que são convenientemente usadas para criar erros. A função errors.New é adequada para criar erros simples. A função fmt.Errorf permite o uso de formatação padrão.


 err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id) 

Normalmente, o tipo de erro é suficiente para lidar com erros. Mas, às vezes, pode ser necessário transmitir informações adicionais com um erro; nesses casos, você pode adicionar seu próprio tipo de erro.
Um bom exemplo é o tipo de PathError do pacote os


 // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } 

O valor desse erro conterá a operação, o caminho e o erro.


Eles são inicializados desta maneira:


 ... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e} 

O processamento pode ter um formulário padrão:


 _, err := os.Open("---") if err != nil{ fmt.Println(err) } // open ---: The system cannot find the file specified. 

Mas, se houver necessidade de obter informações adicionais, você poderá descompactar o erro em * os.PathError :


 _, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) } // Err: The system cannot find the file specified. // Op: open // Path: --- 

A mesma abordagem pode ser aplicada se a função puder retornar vários tipos diferentes de erros.
brincar


Declaração de vários tipos de erros, cada um com seus próprios dados:


código
 type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() } 

Uma função que pode retornar esses erros:


código
 func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil } 

Tratamento de erros através de conversão de tipos:


código
 func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } } 

Caso os erros não precisem de propriedades especiais, é uma boa prática no Go criar variáveis ​​para armazenar erros no nível do pacote. Um exemplo são erros como io.EOF, io.ErrNoProgress e assim por diante.


No exemplo abaixo, interrompemos a leitura e continuamos executando o aplicativo quando o erro é io.EOF ou fechamos o aplicativo para quaisquer outros erros.


 func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } } 

Isso é eficaz porque os erros são gerados apenas uma vez e reutilizados.


rastreamento de pilha


Lista de funções chamadas no momento da captura da pilha. O rastreamento de pilha ajuda a ter uma idéia melhor do que está acontecendo no sistema. Salvar o rastreamento nos logs pode ajudar seriamente na depuração.


O Go muitas vezes não possui essas informações por engano, mas felizmente obter um despejo no Go não é difícil.


Você pode usar debug.PrintStack () para gerar o rastreio na saída padrão:


 func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() } 

Como resultado, as seguintes informações serão gravadas no Stderr:


empilhar
 goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27 

debug.Stack () retorna uma fatia de bytes com um despejo de pilha, que pode ser posteriormente registrado ou em outro lugar.


 b := debug.Stack() fmt.Printf("Trace:\n %s\n", b) 

Há outro ponto se fizermos assim:


 go bar() 

então obtemos as seguintes informações na saída:


 main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c 

Cada goroutine tem uma pilha separada, respectivamente, obtemos apenas seu despejo. A propósito, as goroutines têm suas próprias pilhas, a recuperação ainda está conectada a isso, mas mais sobre isso mais tarde.
E assim, para ver informações sobre todas as goroutines, você pode usar o runtime.Stack () e passar o segundo argumento true.


 func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) } 

empilhar
 Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27 

Adicione essas informações ao erro e, assim, aumente bastante seu conteúdo de informações.
Por exemplo, assim:


 type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() } 

Você pode adicionar uma função para criar este erro:


 func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} } 

Então você já pode trabalhar com isso:


 func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err } 

empilhar
 Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29 

Por conseguinte, o erro e o rastreio podem ser divididos:


 func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } } 

E, claro, já existe uma solução pronta. Um deles é o pacote https://github.com/pkg/errors . Ele permite que você crie um novo erro, que já conterá a pilha de rastreamento, e você pode adicionar um rastreamento e / ou mensagem adicional a um erro existente. Além de formatação de saída conveniente.


 import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") } 

empilhar
 error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 

% v exibirá apenas mensagens


 error2: error 

pânico / recuperação


O pânico (aka acidente, aka pânico), por via de regra, sinaliza a presença de defeitos, devido aos quais o sistema (ou um subsistema específico) não pode continuar funcionando. Se o pânico for chamado, o tempo de execução Go examinará a pilha, tentando encontrar um manipulador para ela.


Pânico não processado finaliza o aplicativo. Isso os diferencia fundamentalmente dos erros que permitem que você não se processe.


Você pode passar qualquer argumento para a chamada de função de pânico.


 panic(v interface{}) 

É conveniente entrar em pânico passar um erro do tipo que simplifica a recuperação e ajuda na depuração.


 panic(errors.New("error")) 

A recuperação de desastre no Go é baseada em uma chamada de função adiada, também conhecida como adiar . Essa função é garantida para ser executada após o retorno da função pai. Independentemente do motivo - a declaração de retorno, o fim da função ou pânico.


E agora a função de recuperação permite obter informações sobre o acidente e interromper o desenrolar da pilha de chamadas.
Uma chamada e tratamento de pânico típicos:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) } 

recuperar a interface de retornos {} (a que passamos para o pânico) ou nula se não houver chamada para entrar em pânico.


Considere outro exemplo de manuseio de emergência. Temos alguma função para a qual transferimos, por exemplo, um recurso e que, em teoria, pode causar pânico.


 func bar(f *os.File) { panic(errors.New("error")) } 

Primeiramente, você pode precisar sempre executar algumas ações no final, por exemplo, limpar recursos, no nosso caso, fechar o arquivo.


Em segundo lugar, a execução incorreta dessa função não deve levar ao final de todo o programa.


Esse problema pode ser resolvido com adiamento, recuperação e fechamento:


 func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error) //   ,   ,     // err := errors.New("trapped panic: %s (%T)", r, r) //     } file.Close() //   }() bar(file) return err } 

O fechamento permite que voltemos às variáveis ​​declaradas acima, graças a isso garantimos fechar o arquivo e, em caso de acidente, extrair um erro dele e passá-lo para o mecanismo usual de tratamento de erros.


Existem situações inversas em que uma função com certos argumentos sempre deve funcionar corretamente e, se isso não acontecer, ficará muito ruim.


Nesses casos, adicione uma função de invólucro na qual a função de destino é chamada e, em caso de erro, o pânico é chamado.


Ir geralmente tem prefixos obrigatórios :


 // MustCompile is like Compile but panics if the expression cannot be parsed. // It simplifies safe initialization of global variables holding compiled regular // expressions. func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp } 

 // Must is a helper that wraps a call to a function returning (*Template, error) // and panics if the error is non-nil. It is intended for use in variable initializations // such as // var t = template.Must(template.New("name").Parse("html")) func Must(t *Template, err error) *Template { if err != nil { panic(err) } return t } 

Vale lembrar mais uma coisa relacionada ao pânico e às goroutines.


Parte das teses do que foi discutido acima:


  • Uma pilha separada é alocada para cada goroutine.
  • Ao chamar pânico, a recuperação é pesquisada na pilha.
  • No caso em que a recuperação não for encontrada, o aplicativo inteiro será encerrado.

O manipulador principal não interceptará o pânico de foo e o programa falhará:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) } 

Isso será um problema se, por exemplo, um manipulador for chamado para conectar-se ao servidor. No caso de pânico em qualquer um dos manipuladores, o servidor inteiro concluirá a execução. E você não pode controlar o tratamento de acidentes nessas funções, por algum motivo.
Em um caso simples, a solução pode ser algo como isto:


 type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) } 

manusear / verificar


Talvez no futuro veremos mudanças no tratamento de erros. Você pode se familiarizar com eles nos links:
go2draft
Tratamento de erros no Go 2


Isso é tudo por hoje. Obrigada

Source: https://habr.com/ru/post/pt422419/


All Articles