Skip to content

panic

panic adalah fungsi built-in go, ketika遇到 kesalahan yang tidak dapat dipulihkan, program seringkali akan melempar panic, seperti akses pointer null yang umum

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

Dalam 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.

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

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

  • link menunjuk ke struktur _panic berikutnya,
  • pc dan sp menunjuk ke eksekusi fungsi pemanggil untuk pemulihan di kemudian hari,
  • arg adalah parameter fungsi panic,
  • argp menunjuk ke parameter defer, ketika terjadi panic akan memicu eksekusi defer
  • aborted menunjukkan apakah dibatalkan

panic seperti defer, juga ada dalam bentuk linked list di goroutine

go
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

go
func gopanic(e any)

Pada awalnya, pertama akan mendeteksi apakah parameter adalah nil, jika nil akan membuat error tipe runtime.PanicNilError

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

Kemudian menambahkan panic saat ini ke kepala linked list goroutine

go
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

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

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

go
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

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 // 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

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
}

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

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

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

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

go
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 = 1

Ia mengatur nilai ret menjadi 1, dari komentar fungsi runtime.deferproc dapat dilihat kode di bawah ini

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

Golang by www.golangdev.cn edit