Not without panic in Go

Hello, dear readers of Habrahabra. While discussing a possible new design of error handling and debating the advantages of explicit error handling, I propose to consider some peculiarities of errors, panics and their recovery in Go, which will be useful in practice.
 
Not without panic in Go
Errors are the values ​​ , and the values ​​can operate and program different logic.
 
In the standard Go library, there are two functions that you can use to create errors. Function errors.New well suited to create simple errors. Function fmt.Errorf allows you to use 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, it is enough to work with errors of type error. But sometimes you may need to send additional information with an error, in such cases you can add your own type of errors.
 
A good example is the type
PathError
from the package 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 ()}

 
The value of this error will contain the operation, path and error.
 
They are initialized in this way:
 

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

 
Processing can have the standard form:
 
_, err: = os.Open ("---")
if err! = nil {
fmt.Println (err)
}
//open ---: The system can not 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:% sn", pe.Err)
fmt.Printf ("Op:% sn", pe.Op)
fmt.Printf ("Path:% sn", pe.Path)
}
//Err: The system can not 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 dаta:
 
code [/b]
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 [/b]
func proc (n int) error {
if n <= 10 {
return & ErrTimeout {Time: time.Second * 1? Err: errors.New ("timeout error")}
} else if n> = 10 {
return & ErrPermission {Status: "access_denied", Err: errors.New ("permission denied")}
}
return nil
}

 
Error handling through casting types:
 
code [/b]
func main () {
err: = proc (11)
if err! = nil {
switch e: = err. (type) {
case * ErrTimeout:
fmt.Printf ("Timeout:% sn", e.Time.String ())
fmt.Printf ("Error:% sn", e.Err)
case * ErrPermission:
fmt.Printf ("Status:% sn", e.Status)
fmt.Printf ("Error:% sn", e.Err)
default:
fmt.Println ("hm?")
os.Exit (1)
}
}
}

 
In the case where errors do not need special properties, in Go it is good practice to create variables to store errors at the package level. Examples include errors such as io.EOF, io.ErrNoProgress and pr
 
In the example below, we stop reading and continue running the application when the error is io.EOF or we close applications 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 created only once and are used repeatedly.
 
stack trace
 
List of functions caused at the time of the capture of the stack. Tracing the stack helps to get a better idea of ​​what's happening in the system. Saving the trace in the logs can seriously help with debugging.
 
The presence of this information in the error of Go is often not enough, but fortunately getting a stack dump in Go is not difficult.
 
To output the trace 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 recorded in Stderr:
 
stack [/b]
goroutine 1[running]:
runtime /debug.Stack (0x? 0x? 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 byte with a dump of the stack, which you can later display in the log or elsewhere.


 
    b: = debug.Stack ()
fmt.Printf ("Trace: n% sn", 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 get only its dump. By the way, about the stacks of gorutin, this is still related work to recover, but more on this later.
 
And so, to see information on all the gorutins, you can use runtime.Stack () and pass the second argument to 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% sn", buf)
}

 
stack [/b]
    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

 

Add to the error this information and thereby greatly enhance its informativeness.
 
For example:


 
    type ErrStack struct {
StackTrace[]byte
Err error
}
func (e * ErrStack) Error () string {
var buf bytes.Buffer
fmt.Fprintf (& buf, "Error: n% sn", e.Err)
fmt.Fprintf (& buf, "Trace: n% sn", 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 this:


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

 
stack [/b]
    Error:
error
Trace:
goroutine 1[running]:
main.NewErrStack (0x4c021f, 0x? 0x4a92e0)
/main.go: 41 + 0xae
main.bar (0xc04207ff3? 0xc04207ff78)
/main.go: 24 + 0x3d
main.foo (0x? 0x48ebff)
/main.go: 21+ 0x29
main.main ()
/main.go: 11 + 0x29

 

Accordingly, the error and trace can be divided:


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

 

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


 
    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 [/b]
    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, signals the presence of problems, due to which the system (or a specific subsystem) can not continue to function. In the case of a panic call, the Go runtime scans the stack, trying to find a handler for it. Unhandled panic stops the application. This fundamentally distinguishes them from errors, which allow not to process themselves.


 

You can pass any argument to the panic function call.


 
    panic (v interface {})    

 

Convenient in panic to convey an error of the type that will simplify the recovery and help debugging.


 
    panic (errors.New ("error"))    

 

Recovery after an accident in Go is based on the deferred function call, it is also defer . Such a function is guaranteed to be executed at the moment of return from the parent function. Regardless of the reason, the return statement, the end of the function or the panic.
 
And here's the function recover gives the opportunity to get information about the accident and stop the unwinding of the call stack.
 
A typical example of a panic call and a 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 we pass to panic) or nil, if there was no panic call.


 

Let's consider one more example of handling emergency situations. We have some function in which we pass for example a resource and which in theory can cause panic.


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

 

First, you may need to always perform some actions at the end, for example, cleaning up resources, in our case, this 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) //handle the emergency situation, unpack it if we know that the panic is error
//err: = errors.New ("trapped panic:% s (% T)", r, r) //or create our own error
}
file.Close () //close the file
} ()
bar (file)
return err
}

 

Closure allows us to address the above declared variables, due to this, we guarantee to close the file and in the event of an accident, extract an error from it and pass it to the usual error handling mechanism.


 

There are reverse situations when a function with certain arguments should always work out correctly and if this does not happen, then what went wrong.
 
In such cases, add a wrapper function in which the target function is called, and in the event of an error, panic is called.
 
In Go, usually these functions are prefixed with Must :


 
    //MustCompile is like Compile but panics if the expression can not be parsed.
//It simplifies the 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 be 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 one more thing, related to panic and gorutins.
 
A part of the theses from the fact that discussed above:


 
  •  
  • For each gorutin, a separate stack is allocated.  
  • When you call panic, the stack looks for recover.  
  • In case, when recover does not find, the whole application is terminated.  

 

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 case of panic in any of the handlers, the entire server will complete the execution. And you can not control the handling of accidents in these functions, for whatever reason, you can not.
 
In a 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 see them through the links:
 
go2draft
 
Error handling in Go 2
 
That's all for today. Thank you!
+ 0 -

Add comment