panic
panic is Go's built-in function. When encountering unrecoverable errors, the program often throws a panic, such as common nil pointer access:
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 dereferenceIn 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.
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.
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:
linkpoints to the next_panicstructurepcandsppoint to the execution context of the calling function for later recoveryargis the parameter of thepanicfunctionargppoints todefer's parameters; whenpanicoccurs, it triggersdeferexecutionabortedindicates whether it was forcibly stopped
panic, like defer, exists in goroutines as a linked list:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
gp.sched.ret = 1It sets the ret value to 1. From the runtime.deferproc function comments, we can see the following content:
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.
