Skip to content

panic

panic Go'nun yerleşik fonksiyonudur. Kurtarılamaz hatalarla karşılaşıldığında, program genellikle bir panic fırlatır, örneğin yaygın nil işaretçi erişimi:

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

Yukarıdaki kodu çalıştırmak programın aşağıdaki panic'i fırlatmasına, sonra programın durmasına neden olur.

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

Bazı durumlarda, daha ciddi sonuçlardan kaçınmak için programın çıkışını sağlamak amacıyla manuel olarak panic fonksiyonunu da çağırırız. Ayrıca panic'i yakalamak için başka bir yerleşik fonksiyon recover'ı kullanırız ve bunu defer ile birlikte kullanırız.

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

Neden recover fonksiyonu defer içinde kullanılmalıdır? recover ne iş yapar? Bu sorular aşağıdaki içerikte cevaplanacaktır.

Yapı

panic'in de runtime'da karşılık gelen bir yapısı vardır, bu runtime._panic'tir. Yapısı karmaşık değildir, aşağıda gösterildiği gibi.

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
}

Yapısı defer ile çok benzerdir:

  • link bir sonraki _panic yapısına işaret eder
  • pc ve sp daha sonra kurtarma için çağırma fonksiyonunun yürütme bağlamına işaret eder
  • arg panic fonksiyonunun parametresidir
  • argp defer'in parametrelerine işaret eder; panic oluştuğunda, defer yürütmesini tetikler
  • aborted zorla durdurulup durdurulmadığını gösterir

panic, defer gibi, bağlı liste olarak goroutine'lerde bulunur:

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

Panic

panic fonksiyonunu aktif olarak çağırsak da veya program bir panic yaşasa da, sonunda runtime.gopanic fonksiyonuna girer:

go
func gopanic(e any)

Başlangıçta, önce parametrenin nil olup olmadığını kontrol eder. Eğer nil ise, bir runtime.PanicNilError tipi hata oluşturur:

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

Sonra mevcut panic'i goroutine'un bağlı listesinin başına ekler:

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

Sonra bir for döngüsüne girer ve mevcut goroutine'un defer bağlı listesini tek tek işlemeye başlar:

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

Eğer mevcut defer zaten başka bir panic tarafından tetiklendiyse, yani _defer.started == true, o zaman daha önceki panic yürütülmez. Sonra defer'e karşılık gelen fonksiyonu yürütür:

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

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

Yürüttükten sonra, mevcut defer yapısını geri dönüştürür ve bir sonraki defer'i yürütmeye devam eder. Tüm defer'ler yürütüldüğünde ve süreçte hiçbiri kurtarılmadığında, runtime.fatalpanic fonksiyonuna girer, bu kurtarılamaz:

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
}

Bu süreçte, panic bilgisini yazdırmak için printpanics çağırılır. Genellikle gördüğümüz çağrı yığını bilgisi bunun tarafından çıktılanır. Son olarak, runtime.exit fonksiyonu programı sistem çağrısı _ExitProcess ile sonlandırır.

Kurtarma

Yerleşik fonksiyon recover'ı çağırarak, derleme sırasında runtime.gorecover fonksiyonuna bir çağrı olur:

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
}

Uygulaması çok basittir, sadece bir şey yapar: p.recovered = true. Kurtarma mantığını işlemekten sorumlu olan kod aslında gopanic fonksiyonundadır:

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

Kurtarma mantığı defer yürütmesinden sonradır. Bu noktada, neden recover fonksiyonunun sadece defer'de kullanılabileceğini anlıyoruz. Eğer recover defer dışında kullanılırsa, gp._panic nil olur ve doğal olarak p.recovered true olarak ayarlanmaz, bu yüzden gopanic fonksiyonu kurtarma mantığına girmez.

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

Kurtarma sırasında, bağlı listede zaten zorla durdurulan panic'leri temizler, sonra runtime.recovery fonksiyonuna girer. runtime.gogo aracılığıyla, kullanıcı fonksiyonunun normal mantık akışına döner:

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

Önemli bir dikkat edilmesi gereken nokta bu satırdır:

go
gp.sched.ret = 1

ret değerini 1 olarak ayarlar. runtime.deferproc fonksiyon yorumlarından, aşağıdaki içeriği görebiliriz:

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

Derleyicinin ürettiği ara kod bu değerin 1 olup olmadığını kontrol eder. Eğer öyleyse, doğrudan runtime.deferreturn fonksiyonunu yürütür. Genellikle, bu fonksiyon sadece fonksiyon dönmeden önce yürütülür, bu da recover'dan sonra fonksiyonun neden doğrudan döndüğünü açıklar.

Golang by www.golangdev.cn edit