panic
panic adalah fungsi built-in go, ketika遇到 kesalahan yang tidak dapat dipulihkan, program seringkali akan melempar panic, seperti akses pointer null yang umum
func main() {
var a *int
*a = 1
}Menjalankan kode di atas, program akan melempar panic berikut, kemudian program akan berhenti.
panic: runtime error: invalid memory address or nil pointer dereferenceDalam beberapa situasi, kita juga akan secara manual memanggil fungsi panic untuk membuat program keluar, sehingga menghindari konsekuensi yang lebih serius. Biasanya juga akan menggunakan fungsi built-in lain recover untuk menangkap panic, dan bekerja sama dengan defer.
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}Mengapa fungsi recover harus digunakan di dalam defer, apa yang dilakukan recover, pertanyaan-pertanyaan ini akan dijawab dalam konten di bawah ini.
Struktur
panic juga memiliki struktur yang sesuai pada runtime, yaitu runtime._panic, strukturnya tidak terlalu kompleks, sebagai berikut.
type _panic struct {
argp unsafe.Pointer // pointer ke argumen pemanggilan defer yang dijalankan selama panic; tidak dapat dipindahkan - diketahui oleh liblink
arg any // argumen ke panic
link *_panic // link ke panic sebelumnya
pc uintptr // tempat kembali di runtime jika panic ini di-bypass
sp unsafe.Pointer // tempat kembali di runtime jika panic ini di-bypass
recovered bool // apakah panic ini sudah selesai
aborted bool // panic ini dibatalkan
goexit bool
}Strukturnya sangat mirip dengan defer,
linkmenunjuk ke struktur_panicberikutnya,pcdanspmenunjuk ke eksekusi fungsi pemanggil untuk pemulihan di kemudian hari,argadalah parameter fungsipanic,argpmenunjuk ke parameterdefer, ketika terjadipanicakan memicu eksekusideferabortedmenunjukkan apakah dibatalkan
panic seperti defer, juga ada dalam bentuk linked list di goroutine
type g struct {
_panic *_panic // panic terdalam - offset diketahui oleh liblink
_defer *_defer // defer terdalam
}
Panic
Baik kita secara aktif memanggil fungsi panic, atau program mengalami panic, pada akhirnya akan masuk ke fungsi runtime.gopanic
func gopanic(e any)Pada awalnya, pertama akan mendeteksi apakah parameter adalah nil, jika nil akan membuat error tipe runtime.PanicNilError
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Kemudian menambahkan panic saat ini ke kepala linked list goroutine
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Kemudian masuk ke loop for untuk mulai memproses linked list defer goroutine saat ini satu per satu
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)))
...
}Jika defer saat ini sudah dipicu oleh panic lain, yaitu _defer.started == true, maka panic yang lebih awal tidak akan dieksekusi. Kemudian mengeksekusi fungsi yang sesuai dengan defer
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)Setelah eksekusi selesai,回收 struktur defer saat ini, lanjutkan mengeksekusi defer berikutnya, setelah selesai mengeksekusi semua struktur defer dan selama periode tidak dipulihkan, akan masuk ke fungsi runtime.fatalpanic, fungsi ini adalah unrecoverable yaitu tidak dapat dipulihkan
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 // tidak tercapai
}Selama periode ini akan membuat printpanics mencetak informasi panic, informasi call stack yang biasanya kita lihat dikeluarkan olehnya, terakhir fungsi runtime.exit keluar dari program melalui system call _ExitProcess.
Pemulihan
Melalui pemanggilan fungsi built-in recover, selama kompilasi akan berubah menjadi pemanggilan fungsi 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
}Implementasinya sangat sederhana, hanya melakukan satu hal p.recovered = true, sedangkan kode yang benar-benar bertanggung jawab untuk menangani logika pemulihan sebenarnya ada di fungsi gopanic
for {
...
d.fn()
...
if p.recovered {
...
}
}Logika pemulihan ada setelah eksekusi defer, sampai di sini sudah mengerti mengapa fungsi recover hanya dapat digunakan di dalam defer, jika menggunakan fungsi recover di luar defer maka gp._panic akan sama dengan nil, secara alami p.recovered tidak akan diatur ke true, maka di fungsi gopanic juga tidak akan masuk ke logika pemulihan ini.
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")
}Saat pemulihan akan membersihkan panic yang sudah dihentikan secara paksa di linked list, kemudian masuk ke fungsi runtime.recovery, oleh runtime.gogo kembali ke alur logika normal fungsi pengguna
func recovery(gp *g) {
// Informasi tentang defer yang dilewatkan dalam struktur 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)
}Kemudian hal penting yang perlu diperhatikan adalah baris kode ini
gp.sched.ret = 1Ia mengatur nilai ret menjadi 1, dari komentar fungsi runtime.deferproc dapat dilihat kode di bawah ini
func deferproc(fn func()) {
...
// deferproc return 0 secara normal.
// fungsi defer yang menghentikan panic
// membuat deferproc return 1.
// kode yang dihasilkan compiler selalu
// memeriksa nilai return dan melompat ke
// akhir fungsi jika deferproc returns != 0.
return0()
}Kode perantara yang dihasilkan compiler akan memeriksa apakah nilai ini adalah 1, jika ya akan langsung mengeksekusi fungsi runtime.deferreturn, biasanya fungsi ini hanya dieksekusi sebelum fungsi return, ini juga menjelaskan mengapa setelah recover fungsi akan langsung return.
