Skip to content

panic

panic — это встроенная функция Go. При столкновении с невосстановимыми ошибками программа часто выбрасывает panic, например, распространённый доступ к nil-указателю:

go
func main() {
  var a *int
  *a = 1
}

Запуск приведённого выше кода приведёт к тому, что программа выбросит следующий panic, затем программа остановится.

panic: runtime error: invalid memory address or nil pointer dereference

В некоторых ситуациях мы также вручную вызываем функцию panic, чтобы программа завершила работу, тем самым избегая более серьёзных последствий. Мы также часто используем другую встроенную функцию recover для перехвата panic, и используем её вместе с defer.

go
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  var a *int
  *a = 1
}

Почему функция recover должна использоваться внутри defer? Какую работу выполняет recover? На эти вопросы будут даны ответы в содержании ниже.

Структура

panic также имеет соответствующую структуру во время выполнения, которая является runtime._panic. Её структура не сложна, как показано ниже.

go
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 указывает на следующую структуру _panic
  • pc и sp указывают на контекст выполнения вызывающей функции для последующего восстановления
  • arg — параметр функции panic
  • argp указывает на параметры defer; когда происходит panic, это запускает выполнение defer
  • aborted указывает, был ли он принудительно остановлен

panic, как и defer, существует в goroutine как связный список:

go
type g struct {
  _panic    *_panic // внутренний panic - смещение известно liblink
  _defer    *_defer // внутренний defer
}

Panic

Активно ли мы вызываем функцию panic или программа испытывает panic, в конечном итоге войдёт в функцию runtime.gopanic:

go
func gopanic(e any)

В начале сначала проверяется, является ли параметр nil. Если это nil, создаётся ошибка типа runtime.PanicNilError:

go
if e == nil {
    if debug.panicnil.Load() != 1 {
        e = new(PanicNilError)
    } else {
        panicnil.IncNonDefault()
    }
}

Затем добавляет текущий panic в голову связного списка goroutine:

go
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

Затем входит в цикл for для начала обработки связного списка defer текущей goroutine по одному:

go
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:

go
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:

go
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 во время компиляции:

go
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:

go
for {
    ...
      d.fn()
      ...
    if p.recovered {
      ...
    }
}

Логика восстановления находится после выполнения defer. В этот момент мы понимаем, почему функция recover может использоваться только в defer. Если recover используется вне defer, gp._panic будет nil, и естественно p.recovered не будет установлен в true, поэтому функция gopanic не войдёт в логику восстановления.

go
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 возвращается к нормальному потоку логики пользовательской функции:

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

Важный момент, на который стоит обратить внимание — эта строка кода:

go
gp.sched.ret = 1

Она устанавливает значение ret в 1. Из комментариев функции runtime.deferproc мы можем видеть следующее содержание:

go
func deferproc(fn func()) {
    ...
  // deferproc возвращает 0 нормально.
  // отложенная функция, которая останавливает panic
  // заставляет deferproc возвращать 1.
  // код, который генерирует компилятор, всегда
  // проверяет возвращаемое значение и переходит к
  // концу функции, если deferproc возвращает != 0.
  return0()
}

Промежуточный код, сгенерированный компилятором, проверяет, равно ли это значение 1. Если да, напрямую выполняет функцию runtime.deferreturn. Обычно эта функция выполняется только перед возвратом функции, что также объясняет, почему функция возвращается напрямую после recover.

Golang by www.golangdev.cn edit