panic
panic — это встроенная функция Go. При столкновении с невосстановимыми ошибками программа часто выбрасывает panic, например, распространённый доступ к nil-указателю:
func main() {
var a *int
*a = 1
}Запуск приведённого выше кода приведёт к тому, что программа выбросит следующий panic, затем программа остановится.
panic: runtime error: invalid memory address or nil pointer dereferenceВ некоторых ситуациях мы также вручную вызываем функцию panic, чтобы программа завершила работу, тем самым избегая более серьёзных последствий. Мы также часто используем другую встроенную функцию recover для перехвата panic, и используем её вместе с defer.
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}Почему функция recover должна использоваться внутри defer? Какую работу выполняет recover? На эти вопросы будут даны ответы в содержании ниже.
Структура
panic также имеет соответствующую структуру во время выполнения, которая является runtime._panic. Её структура не сложна, как показано ниже.
type _panic struct {
argp unsafe.Pointer // указатель на аргументы отложенного вызова, выполняемого во время panic; нельзя перемещать - известно liblink
arg any // аргумент panic
link *_panic // ссылка на более ранний panic
pc uintptr // куда возвращаться в runtime, если этот panic обходится
sp unsafe.Pointer // куда возвращаться в runtime, если этот panic обходится
recovered bool // завершён ли этот panic
aborted bool // был ли этот panic прерван
goexit bool
}Её структура очень похожа на defer:
linkуказывает на следующую структуру_panicpcиspуказывают на контекст выполнения вызывающей функции для последующего восстановленияarg— параметр функцииpanicargpуказывает на параметрыdefer; когда происходитpanic, это запускает выполнениеdeferabortedуказывает, был ли он принудительно остановлен
panic, как и defer, существует в goroutine как связный список:
type g struct {
_panic *_panic // внутренний panic - смещение известно liblink
_defer *_defer // внутренний defer
}
Panic
Активно ли мы вызываем функцию panic или программа испытывает panic, в конечном итоге войдёт в функцию runtime.gopanic:
func gopanic(e any)В начале сначала проверяется, является ли параметр nil. Если это nil, создаётся ошибка типа runtime.PanicNilError:
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Затем добавляет текущий panic в голову связного списка goroutine:
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Затем входит в цикл for для начала обработки связного списка defer текущей goroutine по одному:
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
}Если текущий defer уже был запущен другим panic, т.е. _defer.started == true, тогда более ранний panic не будет выполнен. Затем выполняет функцию, соответствующую defer:
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)После выполнения перерабатывает текущую структуру defer и продолжает выполнять следующий defer. Когда все структуры defer были выполнены и ни одна не была восстановлена в процессе, входит в функцию runtime.fatalpanic, которая является unrecoverable:
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
runningPanicDefers.Add(-1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // не достигнуто
}Во время этого процесса вызывается printpanics для вывода информации о panic. Информация о стеке вызовов, которую мы обычно видим, выводится им. Наконец, функция runtime.exit завершает программу через системный вызов _ExitProcess.
Recovery
Вызов встроенной функции recover становится вызовом функции runtime.gorecover во время компиляции:
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}Её реализация очень проста, делает только одно дело: p.recovered = true. Код, фактически отвечающий за обработку логики восстановления, находится в функции gopanic:
for {
...
d.fn()
...
if p.recovered {
...
}
}Логика восстановления находится после выполнения defer. В этот момент мы понимаем, почему функция recover может использоваться только в defer. Если recover используется вне defer, gp._panic будет nil, и естественно p.recovered не будет установлен в true, поэтому функция gopanic не войдёт в логику восстановления.
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}Во время восстановления очищает те panic в связном списке, которые уже были принудительно остановлены, затем входит в функцию runtime.recovery. Через runtime.gogo возвращается к нормальному потоку логики пользовательской функции:
func recovery(gp *g) {
// Информация о defer, переданная в структуре G.
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}Важный момент, на который стоит обратить внимание — эта строка кода:
gp.sched.ret = 1Она устанавливает значение ret в 1. Из комментариев функции runtime.deferproc мы можем видеть следующее содержание:
func deferproc(fn func()) {
...
// deferproc возвращает 0 нормально.
// отложенная функция, которая останавливает panic
// заставляет deferproc возвращать 1.
// код, который генерирует компилятор, всегда
// проверяет возвращаемое значение и переходит к
// концу функции, если deferproc возвращает != 0.
return0()
}Промежуточный код, сгенерированный компилятором, проверяет, равно ли это значение 1. Если да, напрямую выполняет функцию runtime.deferreturn. Обычно эта функция выполняется только перед возвратом функции, что также объясняет, почему функция возвращается напрямую после recover.
