Не без паники в Go

    Привет, уважаемые читатели Хабрахабра. В то время, как обсуждается возможный новый дизайн обработки ошибок и ведутся споры о преимуществах явной обработки ошибок, предлагаю рассмотреть некоторые особенности ошибок, паник и их восстановления в Go, которые будут полезны на практике.
    image


    error


    error это интерфейс. И как большинство интерфейсов в Go, определение error краткое и простое:


    type error interface {
        Error() string
    }

    Получается любой тип у которого есть метод Error может быть использован как ошибка. Как учил Роб Пайк Ошибки это значения, а значениями можно оперировать и программировать различную логику.


    В стандартной библиотеки Go имеются две функции, которые удобно использовать для создания ошибок. Функция errors.New хорошо подходит для создания простых ошибок. Функция fmt.Errorf позволяет использовать стандартное форматирования.


    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)

    Обычно для работы с ошибками достаточно типа error. Но иногда может потребоваться передавать с ошибкой дополнительную информацию, в таких случаях можно добавить свой тип ошибок.
    Неплохой пример это тип PathError из пакета 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() }

    Значение такой ошибки будет содержать операцию, путь и ошибку.


    Инициализируются они таким образом:


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

    Обработка может иметь стандартный вид:


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

    А вот если есть необходимость получить дополнительную информацию, то можно распаковать error в *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: ---

    Этот же подход можно применять если функция может вернуть несколько различных типов ошибок.
    play


    Объявление нескольких типов ошибок, каждая имеет свои данные:


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

    Функция которая может вернуть эти ошибки:


    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
    }

    Обработка ошибок через приведения типов:


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

    В случае когда ошибкам не нужны специальные свойства, в Go хорошей практикой считается создавать переменные для хранения ошибок на уровне пакетов. Примером может служить такие ошибки как io.EOF, io.ErrNoProgress и проч.


    В примере ниже, прерываем чтение и продолжаем работу приложения, когда ошибка равна io.EOF или закрываем приложения при любых других ошибках.


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

    Это эффективно, поскольку ошибки создаются только один раз и используются многократно.


    stack trace


    Список функций, вызванных в момент захвата стека. Трассировка стека помогает получить более полное представление о происходящем в системе. Сохранение трассировки в логах может серьезно помочь при отладки.


    Наличие этой информации в ошибке у Go часто не хватает, но к счастью получить дампа стека в Go не сложно.


    Для вывода трассировки в стандартный выводов можно воспользоваться debug.PrintStack():


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

    Как результат в 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() возвращает слайс байт с дампом стека, который можно в дальнейшем вывести в журнал или в другом месте.


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

    Есть еще один момент, если мы сделаем вот так:


    go bar()

    то на выходе получим такую информацию:


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

    У каждой горутины отдельный стек, соответственно, мы получаем только его дамп. Кстати, о своих стеках у горутин, с этим еще связана работа recover, но об этом чуть позже.
    И так, что бы увидеть информацию по всем горутинам, можно воспользоваться runtime.Stack() и передать вторым аргументом 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
    

    Добавим в ошибку эту информацию и тем самым сильно повысим ее информативность.
    Например так:


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

    Можно добавить функцию для создания этой ошибки:


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

    Дальше с этим уже можно работать:


    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
    

    Соответственно ошибку и трейс можно рзаделить:


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

    И конечно уже есть готовые решение. Одно из них, это пакет https://github.com/pkg/errors. Он позволяет создавать новую ошибку, которая уже будет содержать стек трейс, а можно добавлять трейс и/или дополнительное сообщения к уже существующей ошибке. Плюс удобное форматирование вывода.


    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 выведет только сообщения


    error2: error

    panic/recover


    Паника(aka авария, aka panic), как правило, сигнализирует о наличии неполадок, из-за которых система (или конкретная подсистема) не может продолжать функционировать. В случае вызова panic среда выполнения Go просматривает стек, пытаясь найти для нее обработчик.


    Необработанные паники прекращают работу приложения. Это принципиально отличает их от ошибок, которые позволяют не обрабатывать себя.


    В вызов функции panic можно передать любой аргумент.


    panic(v interface{})

    Удобно в panic передать ошибку, того типа который упростит восстановления и поможет отладки.


    panic(errors.New("error"))

    Восстановление после аварии в Go основывается на отложенном вызове функций, он же defer. Такая функция гарантировано будет выполнена в момент возврата из родительской функции. Не зависимо от причины — оператор return, конец функции или паника.


    А вот уже функция recover дает возможность получить информацию об аварии и остановить раскручивание стека вызовов.
    Типичный пример вызова panic и обработчик:


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

    recover возвращает interface{} (тот самый который передаем в panic) или nil, если не было вызова panic.


    Рассмотрим еще один пример обработки аварийных ситуаций. У нас есть некоторая функция в которую мы передаем например ресурс и которая в теории может вызвать панику.


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

    Во-первых, может понадобится всегда выполнять какие то действия при завершении, например очистка ресурсов, в нашем случае это закрытия файла.


    Во-вторых, некорректное выполнение такой функции не должно приводить к завершению всей программы.


    Такую задачу можно решить с помощью defer, recover и замыкания:


    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
    }

    Замыкание позволяем обратится к выше объявленным переменным, благодаря этому гарантировано закрываем файл и в случае аварии, извлечь из нее ошибку и передать ее обычному механизму обработки ошибок.


    Бывают обратные ситуации, когда функция c определенными аргументами всегда должна отрабатывать корректно и если этого не происходит, то что пошло совсем плохо.


    В подобных случаях добавляют функцию обертку в которой вызывается целевая функция и в случае ошибки вызывается panic.


    В Go обычно такие функции с префиксом 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
    }

    Стоит помнить еще про один момент, связанный с panic и горутинами.


    Часть тезисов из того что обсудили выше:


    • Для каждой горутины выделяется отдельный стек.
    • При вызове panic, в стеке ищется recover.
    • В случае, когда recover не найдет, завершается все приложение.

    Обработчик в main не перехватит панику из foo и программа аварийно завершится:


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

    Это будет проблемой, если например вызываются обработчик для соединения на сервере. В случае паники в любом из обработчиков, весь сервер завершит выполнение. А контролировать обработку аварий в этих функциях, по какой то причине, вы не можете.
    В простом случае решение может выглядит примерно так:


    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


    Возможно в будущем нас ждут изменения в обработки ошибок. Ознакомится с ними можно по ссылкам:
    go2draft
    Обработка ошибок в Go 2


    На сегодня все. Спасибо!

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 4

      0
      Ну вот Go добрался до энтерпрайза. Когда nj казавшаяся гениальной конструкцией
      varResult, errCode = func()
      разбилась о реальности разработки – что надо не только обрабатывать varResult на месте, но и еще обрабатывать на месте errCode. Ход гениален, но вот алгоритм то у нас «однопоточный» (имеется ввиду листинг программы). Но люди помнящие Basic очень хорошо помнят что значит обрабатывать ошибку в месте ее возникновения.
      А потом придет еще пушистый зверек когда какой ни будь умник захочет сделать errCode не целочисленным. И выяснится что динамическое типизирование ой как сложно поддерживать в промышленных объемах. И понесётся и понесется…
        +1

        Почему в стандартных библиотеках для константных ошибок используется переменная, создаваемая через errors.New() а не, например, переопределение типа строки?
        В таком случае переопределить значение будет невозможно даже случайно.


        package errors
        
        type Error string
        
        func (e Error) Error() string {
            return string(e)
        }
        
        //
        const ErrFileNotFound errors.Error = "file not found"

        Допустим, в стандартных библиотеках этого нельзя сделать из-за того, что это может сломать обратную совместимость для некоторого кода.
        Но в примерах и многих библиотеках такой подход тоже не используется по каким-то причинам.

          0

          Возможно не до конца понял вопрос, но что касается errors.New(). Он возвращает указатель на структуру с закрытым полем типа строка.
          https://golang.org/src/errors/errors.go
          Соответственно мы сравниваем указатели, а не строки, со всеми вытекающими от сюда.
          play


          func main() {
              if io.EOF == errors.New("EOF"){
                  fmt.Printf("case 1")
              }
              if errors.New("EOF") == errors.New("EOF"){
                  fmt.Printf("case 2")
              }
          }

          Например, ошибки из разных пакетов не будет равны, да же если у них одно содержание.

            0
            Дейв Чейни как раз рекомендовал такой подход в своей статье. Я в своих проектах стараюсь использовать именно такой подход.

          Only users with full accounts can post comments. Log in, please.