Показаны сообщения с ярлыком использование panic в Go. Показать все сообщения
Показаны сообщения с ярлыком использование panic в Go. Показать все сообщения

воскресенье, 22 ноября 2020 г.

Go style guides: не используйте panic

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

Неудачный вариант:

func run(args []string) {
    if len(args) == 0 {
        panic("требуется аргумент")
    }
    // ...
}

func main() {
    run(os.Args[1:])
}

Более удачный вариант:

func run(args []string) error {
    if len(args) == 0 {
        return errors.New("требуется аргумент")
    }
    // ...
    return nil
}

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Panic/recover не является стратегией обработки ошибок. Программа должна паниковать только тогда, когда происходит что-то безвозвратное, например, нулевое разыменование (nil dereference, попытка получить значение по nil адресу, например в ситуации когда переменная с типом указателя структуры содержит nil и происходит попытка разыменования указателя). Исключением является инициализация программы: плохие вещи при запуске программы, которые должны прервать выполнение программы, могут вызвать panic.

var _statusTemplate = template.Must(
     template.New("name").Parse("_statusHTML"))

Даже в тестах предпочтительнее паники t.Fatal или t.FailNow, чтобы тест был отмечен как неудачный.

Неудачный вариант:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
    panic("не удалось настроить тест")
}

Более удачный вариант:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
    t.Fatal("не удалось настроить тест")
}


Читайте также:


воскресенье, 3 мая 2020 г.

Паника, следы стека и способы восстановления в Golang

Паника - исключение в Go

Паника (panic) похожа на исключения (exceptions) C++ и Java, но предназначена только для ошибок времени выполнения, таких как следование по нулевому указателю или попытка индексировать массив за пределами границ. Для обозначения таких событий, как конец файла (EOF), программы Go используют встроенный тип error.

Паника останавливает нормальное выполнение goroutine:

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

Паника вызвана либо ошибкой во время выполнения, либо явным вызовом встроенной функции panic.

Следы стека

Трассировка стека - отчет обо всех активных кадрах стека - обычно выводится на консоль при возникновении паники. Следы стека могут быть очень полезны для отладки:

  • вы не только видите, где произошла ошибка,
  • но и как программа прибыла в это место.

Интерпретация трассировки стека

Вот пример трассировки стека:

goroutine 11 [running]:
testing.tRunner.func1(0xc420092690)
    /usr/local/go/src/testing/testing.go:711 +0x2d2
panic(0x53f820, 0x594da0)
    /usr/local/go/src/runtime/panic.go:491 +0x283
github.com/yourbasic/bit.(*Set).Max(0xc42000a940, 0x0)
    ../src/github.com/bit/set_math_bits.go:137 +0x89
github.com/yourbasic/bit.TestMax(0xc420092690)
    ../src/github.com/bit/set_test.go:165 +0x337
testing.tRunner(0xc420092690, 0x57f5e8)
    /usr/local/go/src/testing/testing.go:746 +0xd0
created by testing.(*T).Run
    /usr/local/go/src/testing/testing.go:789 +0x2de

Это можно прочитать снизу вверх:

  • testing.(*T).Run вызвал testing.tRunner,
  • который вызвал bit.TestMax,
  • который вызвал bit.(*Set).Max,
  • который вызвал panic,
  • который вызвал testing.tRunner.func1.

Строки с отступом показывают исходный файл и номер строки, по которой была вызвана функция. Шестнадцатеричные числа относятся к значениям параметров, включая значения указателей и внутренних структур данных.

Распечатать и записать трассировку стека

Чтобы напечатать трассировку стека для текущей программы, используйте debug.PrintStack из пакета runtime/debug.

Вы также можете программно проверить текущую трассировку стека, вызвав runtime.Stack.

Уровень деталей

Переменная GOTRACEBACK управляет количеством выходных данных, генерируемых при сбое программы Go.

  • GOTRACEBACK=none пропускает трассировки стека goroutine полностью.
  • GOTRACEBACK=single (по умолчанию) печатает трассировку стека для текущей go-процедуры (goroutine), исключая функции, внутренние для системы времени выполнения. Ошибка выводит трассировки стека для всех go-процедур, если текущая go-процедура не существует или ошибка является внутренней по отношению к среде выполнения.
  • GOTRACEBACK=all добавляет трассировки стека для всех пользовательских go-процедур.
  • GOTRACEBACK=system, как all, но добавляет кадры стека для функций времени выполнения (run-time) и показывает go-процедуры, созданные внутри во время выполнения.

Recover и поимка паники

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

  • Вызов recover останавливает раскручивание и возвращает аргумент, переданный в panic.
  • Если goroutine не паникует, recover возвращает nil.

Поскольку единственный код, который выполняется при разматывании, находится внутри отложенных (defer) функций, recover полезна только внутри таких функций.

Пример обработчика паники

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    m := 1
    panic("foo: fail")
    m = 2
    return m
}

Вывод:

foo: fail
main received 0

Поскольку паника возникла до того, как foo вернула значение, n по-прежнему имеет начальное нулевое значение.

Вернуть значение

Чтобы вернуть значение во время паники, вы должны использовать именованное возвращаемое значение.

func main() {
    n := foo()
    fmt.Println("main received", n)
}

func foo() (m int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
            m = 2
        }
    }()
    m = 1
    panic("foo: fail")
    m = 3
    return m
}

Вывод:

foo: fail
main received 2

Тест паники (утилитная функция)

В этом примере мы используем отражение, чтобы проверить, имеют ли список переменных интерфейса типы, соответствующие параметрам данной функции. Если это так, мы вызываем функцию с этими параметрами, чтобы проверить, есть ли паника.

// Panics сообщает, паникует ли функция f с параметрами p.
func Panics(f interface{}, p ...interface{}) bool {
    fv := reflect.ValueOf(f)
    ft := reflect.TypeOf(f)
    if ft.NumIn() != len(p) {
        panic("неверное количество аргументов")
    }
    pv := make([]reflect.Value, len(p))
    for i, v := range p {
        if reflect.TypeOf(v) != ft.In(i) {
            panic("неверный тип аргумента")
        }
        pv[i] = reflect.ValueOf(v)
    }
    return call(fv, pv)
}

func call(fv reflect.Value, pv []reflect.Value) (b bool) {
    defer func() {
        if err := recover(); err != nil {
            b = true
        }
    }()
    fv.Call(pv)
    return
}


Читайте также:


четверг, 30 апреля 2020 г.

Отложить вызов функции (с возвращаемым значением) в Golang

Оператор defer откладывает выполнение функции до тех пор, пока не вернется окружающая функция, либо в обычном режиме, либо через panic.

func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}

Вывод:

Hello
World

Отложенные вызовы выполняются даже при панике функции:

func main() {
    defer fmt.Println("World")
    panic("Stop")
    fmt.Println("Hello")
}

Вывод:

World
panic: Stop

goroutine 1 [running]:
main.main()
    ../main.go:3 +0xa0

Порядок исполнения

Аргументы отложенного вызова вычисляются немедленно, даже если вызов функции не выполняется до тех пор, пока не вернется окружающая функция.

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

func main() {
    fmt.Println("Hello")
    for i := 1; i <= 3; i++ {
        defer fmt.Println(i)
    }
    fmt.Println("World")
}

Вывод:

Hello
World
3
2
1

Используйте func для возврата значения

Отложенные анонимные функции могут получать доступ и изменять именованные возвращаемые параметры окружающей функции.

В этом примере функция foo возвращает "Change World".

func foo() (result string) {
    defer func() {
        // изменяет значение в самый последний момент
        result = "Change World" 
    }()
    return "Hello World"
}

Применение defer

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

Закрытие файла

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

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

Обработка ошибок: поймать panic

Defer также может быть использован для восстановления (recover) после panic и обновления возвращаемого значения.


Читайте также:


четверг, 13 февраля 2020 г.

Defer, Panic, и Recover в Golang

Go имеет обычные механизмы управления потоком: if, for, switch, goto. Он также содержит утверждение (statement) go для запуска кода в отдельной goroutine. Но в этом посте мы обсудим некоторые из менее распространенных утверждений для управления потоком исполнения программы в Go: defer, panic, и recover.

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

Например, давайте посмотрим на функцию, которая открывает два файла и копирует содержимое одного файла в другой:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

Это работает, но есть ошибка. Если вызов os.Create завершится неудачно, функция вернется без закрытия исходного файла. Это можно легко исправить, поместив вызов src.Close перед вторым return утверждением, но если бы функция была более сложной, проблему было бы не так легко заметить и решить. Вводя defer утверждения, мы можем гарантировать, что файлы всегда закрыты:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

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

Поведение defer утверждений является простым и предсказуемым. Есть три простых правила:

1. Аргументы отложенной функции оцениваются, когда оценивается оператор defer.

В этом примере выражение "i" вычисляется при отсрочке вызова Println. Отложенный вызов выведет "0" после возврата из функции.

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2. Отложенные вызовы функций выполняются в порядке "последний пришел - первый вышел" (Last In First Out, LIFO) после возврата окружающей функции.

Эта функция печатает "3210":

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

3. Отложенные функции могут читать и присваивать возвращаемой функции именованные возвращаемые значения.

В этом примере отложенная функция увеличивает возвращаемое значение i после возврата окружающей функции. Таким образом, эта функция возвращает 2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

Это удобно для изменения значения ошибки, возвращаемого функцией; мы увидим пример этого в ближайшее время.

Panic - это встроенная функция, которая останавливает обычный поток контроля и начинает паниковать. Когда функция F вызывает panic, выполнение F останавливается, все отложенные (defer) функции в F выполняются нормально, а затем F возвращается к своему вызывающему. Для вызывающей стороны F ведет себя как вызов panic. Процесс продолжается вверх по стеку до тех пор, пока не будут возвращены все функции в текущей процедуре, после чего программа завершится сбоем. Panic можно инициировать, вызывая panic напрямую. Они также могут быть вызваны ошибками во время выполнения, такими как доступ за пределы массива.

Recover - это встроенная функция, которая восстанавливает контроль над паникующими goroutine. Recover полезна только внутри отложенных (defer) функций. Во время обычного выполнения, вызов recover вернет nil и не будет иметь никакого другого эффекта. Если текущая goroutine вызывает panic, вызов recover захватит значение, переданное panic, и возобновит нормальное выполнение.

Вот пример программы, которая демонстрирует механику panic и recover:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

Функция g принимает int i и паникует, если i больше 3, иначе она вызывает себя с аргументом i + 1. Функция f откладывает функцию, которая вызывает recover и печатает восстановленное значение (если оно не равно нулю).

Программа выведет:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Если мы удалим отложенную (defer) функцию из f, panic не восстановится и достигнет вершины стека вызовов программы, завершив программу. Эта измененная программа выведет:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
 
panic PC=0x2a9cd8
[stack trace omitted]

Для реального примера panic и recover можно посмотреть пакет json из стандартной библиотеки Go. Он кодирует интерфейс с набором рекурсивных функций. Если при обходе значения возникает ошибка, вызывается паника, чтобы развернуть стек для вызова функции верхнего уровня, который восстанавливается после паники и возвращает соответствующее значение ошибки.

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

Другие способы использования defer (кроме приведенный выше примера file.Close) включают освобождение мьютекса:

mu.Lock()
defer mu.Unlock()

печать нижнего колонтитула:

printHeader()
defer printFooter()

и другие.

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


Читайте также:


четверг, 28 февраля 2019 г.

Go Code Review Comments: комментарии к документу, panic, строки ошибок, примеры

Комментарии к документу (Doc comments)

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

Не используйте panic напрасно

Не используйте панику для нормальной обработки ошибок. Используйте ошибку и несколько возвращаемых значений.

Строки ошибок

Строки ошибок не должны начинаться с заглавной буквы (если только они не начинаются с собственных имен или аббревиатур) или заканчиваться пунктуацией, поскольку они обычно печатаются в другом контексте. То есть используйте fmt.Errorf("something bad") , а не fmt.Errorf("Something bad"), так что log.Printf("Reading %s: %v", filename, err) форматируется без ложной заглавной буквы в середине сообщения. Это не относится к сообщениям журнала (logging), которые ориентированы на использование сообщения как отдельной строки и не объединяются внутри других сообщений.

Обработка ошибок

Не отбрасывайте ошибки, используя пустые переменные _. Если функция возвращает ошибку, проверьте ее, чтобы убедиться, что функция выполнена успешно. Обработайте ошибку, верните ее или, в действительно исключительных ситуациях, используйте panic.

Примеры

При добавлении нового пакета включайте примеры предполагаемого использования: работоспособный пример или простой тест, демонстрирующий полную последовательность вызовов.


Читайте также: