panic
panic là hàm built-in của go, khi gặp phải các lỗi không thể phục hồi, chương trình thường sẽ ném ra panic, ví dụ như truy cập con trỏ rỗng thường gặp
func main() {
var a *int
*a = 1
}Chạy đoạn code trên, chương trình sẽ ném ra panic như sau, rồi chương trình sẽ dừng lại.
panic: runtime error: invalid memory address or nil pointer dereferenceTrong một số trường hợp, chúng ta cũng sẽ thủ công gọi hàm panic để khiến chương trình thoát, nhằm tránh hậu quả nghiêm trọng hơn. Thông thường cũng sẽ dùng một hàm built-in khác là recover để bắt panic, và kết hợp sử dụng với defer.
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}Tại sao hàm recover bắt buộc phải sử dụng trong defer, recover đã làm những công việc gì, những vấn đề này đều sẽ được giải đáp trong nội dung dưới đây.
Cấu trúc
panic trong runtime cũng có cấu trúc tương ứng để biểu diễn, đó là runtime._panic, cấu trúc của nó không phức tạp, như sau.
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
}Cấu trúc của nó rất giống với defer,
linktrỏ đến cấu trúc_panictiếp theo,pcvàsptrỏ đến hiện trường thực thi của hàm gọi để tiện phục hồi sau này,arglà tham số của hàmpanic,argptrỏ đến tham số củadefer, khi xảy rapanicsẽ kích hoạt thực thideferabortedbiểu thị nó có bị強制 dừng không
panic cũng giống như defer, tồn tại dưới hình thức danh sách liên kết trong goroutine
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
Panic
Bất kể là chúng ta chủ động gọi hàm panic, hay là panic do chương trình xảy ra, cuối cùng đều sẽ đi vào hàm runtime.gopanic
func gopanic(e any)Khi bắt đầu, trước tiên sẽ kiểm tra tham số có phải là nil không, nếu là nil thì sẽ new một lỗi kiểu runtime.PanicNilError
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Sau đó thêm panic hiện tại vào đầu danh sách liên kết của goroutine
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Rồi đi vào vòng lặp for bắt đầu xử lý từng cái defer trong danh sách liên kết defer của goroutine hiện tại
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)))
...
}Nếu defer hiện tại đã bị panic khác kích hoạt, tức _defer.started == true, thì panic sớm hơn sẽ không được thực thi. Rồi thực thi hàm tương ứng với defer
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)Sau khi thực thi xong thì thu hồi cấu trúc defer hiện tại, tiếp tục thực thi defer tiếp theo, khi thực thi xong toàn bộ cấu trúc defer mà trong thời gian không được phục hồi, sẽ đi vào hàm runtime.fatalpanic, hàm này là unrecoverable tức không thể phục hồi
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
}Trong thời gian này sẽ khiến printpanics in thông tin panic, thông tin call stack mà chúng ta thường thấy là do nó xuất ra, cuối cùng do hàm runtime.exit thông qua system call _ExitProcess thoát chương trình.
Phục hồi
Thông qua gọi hàm built-in recover, trong thời gian biên dịch sẽ biến thành lời gọi đến hàm 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
}Phần triển khai của nó rất đơn giản, chỉ làm mỗi việc p.recovered = true, mà code thực sự负责 xử lý logic phục hồi thực tế nằm trong hàm gopanic
for {
...
d.fn()
...
if p.recovered {
...
}
}Logic phục hồi nằm sau khi defer thực thi, đến đây cũng đã hiểu tại sao hàm recover chỉ có thể sử dụng trong defer, nếu sử dụng hàm recover bên ngoài defer thì gp._panic sẽ bằng nil, tự nhiên p.recovered sẽ không được đặt thành true, thì trong hàm gopanic cũng sẽ không đi vào logic phục hồi này.
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")
}Khi phục hồi sẽ dọn dẹp những panic đã bị強制 dừng trong danh sách liên kết, rồi đi vào hàm runtime.recovery, do runtime.gogo trở về luồng logic bình thường của hàm người dùng
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)
}Rồi có một điểm chú ý quan trọng là dòng code này
gp.sched.ret = 1Nó đặt giá trị ret thành 1, từ phần chú thích hàm của hàm runtime.deferproc có thể thấy nội dung dưới đây
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()
}Code trung gian mà compiler sinh ra sẽ kiểm tra giá trị này có phải là 1 không, nếu là thì sẽ trực tiếp thực thi hàm runtime.deferreturn, thông thường hàm này chỉ được thực thi trước khi hàm trả về, điều này cũng giải thích tại sao sau khi recover xong thì hàm sẽ trực tiếp trả về.
