Skip to content

panic

panic is Go's built-in function. When encountering unrecoverable errors, the program often throws a panic, such as common nil pointer access:

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

Running the code above will cause the program to throw the following panic, then the program will stop.

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

In some situations, we also manually call the panic function to make the program exit, thereby avoiding more serious consequences. We also often use another built-in function recover to catch panic, and use it together with defer.

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

Why must the recover function be used inside defer? What work does recover do? These questions will be answered in the content below.

Structure

panic also has a corresponding structure at runtime, which is runtime._panic. Its structure is not complex, as shown below.

go
type _panic struct {
  argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
  arg       any            // argument to panic
  link      *_panic        // link to earlier panic
  pc        uintptr        // where to return to in runtime if this panic is bypassed
  sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
  recovered bool           // whether this panic is over
  aborted   bool           // the panic was aborted
  goexit    bool
}

Its structure is very similar to defer:

  • link points to the next _panic structure
  • pc and sp point to the execution context of the calling function for later recovery
  • arg is the parameter of the panic function
  • argp points to defer's parameters; when panic occurs, it triggers defer execution
  • aborted indicates whether it was forcibly stopped

panic, like defer, exists in goroutines as a linked list:

go
type g struct {
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
}

Panic

Whether we actively call the panic function or the program experiences a panic, it will eventually enter the runtime.gopanic function:

go
func gopanic(e any)

At the beginning, it first checks if the parameter is nil. If it's nil, it creates a runtime.PanicNilError type error:

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

Then it adds the current panic to the head of the goroutine's linked list:

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

Then it enters a for loop to start processing the current goroutine's defer linked list one by one:

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

If the current defer has already been triggered by another panic, i.e., _defer.started == true, then the earlier panic will not be executed. Then it executes the function corresponding to defer:

go
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil

d.fn = nil
gp._defer = d.link
freedefer(d)

After execution, it recycles the current defer structure and continues to execute the next defer. When all defer structures have been executed and none were recovered during the process, it enters the runtime.fatalpanic function, which is 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 // not reached
}

During this process, printpanics is called to print panic information. The call stack information we usually see is output by it. Finally, the runtime.exit function exits the program through the system call _ExitProcess.

Recovery

By calling the built-in function recover, it becomes a call to the runtime.gorecover function during compilation:

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
}

Its implementation is very simple, only doing one thing: p.recovered = true. The code actually responsible for handling the recovery logic is in the gopanic function:

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

The recovery logic is after defer execution. At this point, we understand why the recover function can only be used in defer. If recover is used outside of defer, gp._panic would be nil, and naturally p.recovered would not be set to true, so the gopanic function would not enter the recovery logic.

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

During recovery, it cleans up those panics in the linked list that have already been forcibly stopped, then enters the runtime.recovery function. Through runtime.gogo, it returns to the normal logic flow of the user function:

go
func recovery(gp *g) {
  // Info about defer passed in G struct.
  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)
}

An important point to note is this line of code:

go
gp.sched.ret = 1

It sets the ret value to 1. From the runtime.deferproc function comments, we can see the following content:

go
func deferproc(fn func()) {
    ...
  // deferproc returns 0 normally.
  // a deferred func that stops a panic
  // makes the deferproc return 1.
  // the code the compiler generates always
  // checks the return value and jumps to the
  // end of the function if deferproc returns != 0.
  return0()
}

The intermediate code generated by the compiler checks if this value is 1. If so, it directly executes the runtime.deferreturn function. Usually, this function is only executed before the function returns, which also explains why the function returns directly after recover.

Golang by www.golangdev.cn edit