📜 ⬆️ ⬇️

Don't panic in Go

Hello, dear readers Habrahabra. While a possible new error handling design is being discussed and disputes are underway about the advantages of explicit error handling, I suggest considering some features of errors, panic and their recovery in Go, which will be useful in practice.
image


error


error is an interface. And like most interfaces in Go, the definition of an error is short and simple:


type error interface { Error() string } 

Any type that has an Error method is obtained can be used as an error. As Rob Pike taught, Errors are values , and values ​​can be manipulated and programmed by various logic.


The standard Go library has two functions that are convenient to use for creating errors. The errors.New function is good for creating simple errors. The fmt.Errorf function allows using standard formatting.


 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) 

Usually, to work with errors, type error is sufficient. But sometimes it may be necessary to transmit additional information with an error, in such cases you can add your type of errors.
A good example is the type of PathError from the os package.


 // 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() } 

The value of such an error will contain an operation, a path and an error.


They are initialized this way:


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

Processing can have a standard view:


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

But if there is a need to get additional information, then you can unpack the error in * 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: --- 

The same approach can be used if the function can return several different types of errors.
play


Declaring several types of errors, each has its own data:


code
 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() } 

A function that can return these errors:


code
 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 } 

Error handling through type conversions:


code
 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) } } } 

In the case when errors do not need special properties, in Go it is a good practice to create variables to store errors at the packet level. Examples include errors such as io.EOF, io.ErrNoProgress, and so on.


In the example below, we interrupt the reading and continue the operation of the application when the error is io.EOF or close the application for any other errors.


 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) } } } 

This is effective because errors are generated only once and are reused many times.


stack trace


The list of functions called at the moment the stack is captured. Tracing the stack helps to get a better idea of ​​what is happening in the system. Saving traces in logs can seriously help with debugging.


Having this information in error for Go is often not enough, but fortunately getting a dump of the stack in Go is not difficult.


To output traces to standard outputs, you can use debug.PrintStack () :


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

As a result, the following information will be written to Stderr:


stack
 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 () returns a slice of bytes with a stack dump, which can later be displayed in a log or in another place.


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

There is one more thing if we do this:


 go bar() 

then at the output we get the following information:


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

Each gorutina has a separate stack, respectively, we only get its dump. By the way, about their stacks at Gorutin, the work of recover is still associated with this, but more on that later.
And so, to see the information on all the gorutines, you can use runtime.Stack () and pass the second argument 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) } 

stack
 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 

We add this information to the error and thereby greatly increase its information content.
For example:


 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() } 

You can add a function to create this error:


 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)} } 

Then you can work with it:


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

stack
 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 

Accordingly, the error and the trace can be broken:


 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) } } 

And of course there is already a ready solution. One of them is the https://github.com/pkg/errors package. It allows you to create a new error that will already contain a stack of traces, or you can add a trace and / or additional messages to an already existing error. Plus convenient formatting output.


 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") } 

stack
 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 will display only messages


 error2: error 

panic / recover


Panic (aka accident, aka panic), as a rule, indicates the presence of problems due to which the system (or a particular subsystem) cannot continue to function. In the case of a panic call, Go runtime scans the stack, trying to find a handler for it.


Untreated panic stops the application. This fundamentally distinguishes them from errors that make it possible not to process themselves.


Any argument can be passed to the panic function call.


 panic(v interface{}) 

Conveniently in panic to pass an error, of the type that will simplify recovery and help debugging.


 panic(errors.New("error")) 

Disaster recovery in Go is based on a deferred function call, also defer . This function is guaranteed to be executed at the time of return from the parent function. Regardless of the reason - the operator return, end of the function or panic.


But the recover function already provides an opportunity to get information about the accident and stop the unwinding of the call stack.
A typical example of a panic call and handler is:


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

recover returns interface {} (the one that is passed to panic) or nil if there was no panic call.


Consider another example of handling emergency situations. We have a certain function in which we transfer, for example, a resource and which, in theory, can cause a panic.


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

First, it may be necessary to always perform some actions at the end, for example, cleaning up resources, in our case it is closing the file.


Secondly, the incorrect execution of such a function should not lead to the completion of the entire program.


Such a problem can be solved with the help of defer, recover and closure:


 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 } 

We allow the closure to address the above declared variables, thanks to which it is guaranteed to close the file and in case of an accident, extract the error from it and transfer it to the usual error handling mechanism.


There are reverse situations where a function with certain arguments should always work correctly and if this does not happen, then what went really bad.


In such cases, a wrapper function is added in which the target function is called and panic is called in case of an error.


In Go, usually such functions with the prefix Must :


 // 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 } 

It is worth remembering about one more moment related to panic and gorutines.


Part of the theses from what was discussed above:



The handler in main will not intercept the panic from foo and the program will crash:


 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")) } 

This will be a problem if, for example, a handler is called to connect to the server. In the event of a panic in any of the handlers, the entire server will terminate. And for some reason, you cannot control the handling of accidents in these functions.
In the simple case, the solution might look something like this:


 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")) } 

handle / check


Perhaps in the future we are waiting for changes in error handling. You can get acquainted with them by the links:
go2draft
Error handling in Go 2


That's all for today. Thank!


')

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


All Articles