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 propose to consider 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, the error type 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:


    ...
    returnnil, &PathError{"open", name, syscall.ENOENT}
    ...
    returnnil, &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
    funcproc(n int)error {
        if n <= 10 {
            return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")}
        } elseif n >= 10 {
            return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")}
        }
        returnnil
    }

    Error handling through type conversions:


    code
    funcmain(){
        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 for storing 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.


    funcmain(){
        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 stack on Go is not difficult.


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


    funcmain(){
        foo()
    }
    funcfoo(){
        bar()
    }
    funcbar(){
        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.


    funcbar(){
        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:


    funcNewErrStack(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:


    funcmain() {
        err := foo()
        if err != nil {
            fmt.Println(err)
        }
    }
    funcfoo()error{
        return bar()
    }
    funcbar()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:


    funcmain(){
        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, and you can add a trace and / or additional messages to an already existing error. Plus convenient formatting output.


    import (
        "fmt""github.com/pkg/errors"
    )
    funcmain(){
        err := foo()
        if err != nil {
            fmt.Printf("%+v", err)
        }
    }
    funcfoo()error{
        err := bar()
        return errors.Wrap(err, "error2")
    }
    funcbar()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, signals 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 allow you not to handle yourself.


    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 the recovery and help debugging.


    panic(errors.New("error"))

    Disaster recovery in Go is based on a deferred function call, also defer . Such a 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 uncoiling of the call stack.
    A typical example of a panic call and handler is:


    funcmain(){
        deferfunc() {
            if err := recover(); err != nil{
                fmt.Printf("panic: %s", err)
            }
        }()
        foo()
    }
    funcfoo(){
        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.


    funcbar(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:


    funcfoo()(err error) {
        file, _ := os.Open("file")
        deferfunc() {
            if r := recover(); r != nil {
                err = r.(error) // обрабатываем аварийную ситуацию, распаковываем если знаем, что в панике ошибка// err := errors.New("trapped panic: %s (%T)", r, r) // или создаем свою ошибку
            }
            file.Close() // закрываем файл
        }()
        bar(file)
        return err
    }

    The closure allows you to turn to 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.funcMustCompile(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"))funcMust(t *Template, err error) *Template {
        if err != nil {
            panic(err)
        }
        return t
    }

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


    Part of the theses from what was discussed above:


    • For each gorutina a separate stack is allocated.
    • When calling panic, recover is looked up in the stack.
    • In the case when recover does not find, the entire application is terminated.

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


    funcmain(){
        deferfunc() {
            if err := recover(); err != nil{
                fmt.Printf("panic: %s", err)
            }
        }()
        go foo()
        time.Sleep(time.Minute)
    }
    funcfoo(){
        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()funcDef(fn f) {
        gofunc() {
            deferfunc() {
                if err := recover(); err != nil {
                    log.Println("panic")
                }
            }()
            fn()
        }()
    }
    funcmain() {
        Def(foo)
        time.Sleep(time.Minute)
    }
    funcfoo() {
        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!


    Also popular now: