Skip to content

panic

panic è una funzione built-in di Go. Quando si incontrano errori non recuperabili, il programma spesso lancia un panic, come il comune accesso a puntatore nullo:

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

Eseguendo il codice sopra, il programma lancerà il seguente panic e si fermerà:

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

In alcuni casi, chiamiamo manualmente la funzione panic per far uscire il programma, evitando così conseguenze più gravi. Spesso usiamo un'altra funzione built-in recover per catturare panic, in combinazione con defer.

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

Perché la funzione recover deve essere usata dentro defer, e cosa fa esattamente recover? Queste domande troveranno risposta nei contenuti seguenti.

Struttura

panic ha anche una struttura corrispondente a runtime, ovvero runtime._panic. La sua struttura non è complessa:

go
type _panic struct {
  argp      unsafe.Pointer // puntatore agli argomenti della chiamata defer eseguita durante panic; non può essere spostato - noto a liblink
  arg       any            // argomento del panic
  link      *_panic        // collegamento al panic precedente
  pc        uintptr        // dove tornare a runtime se questo panic viene bypassato
  sp        unsafe.Pointer // dove tornare a runtime se questo panic viene bypassato
  recovered bool           // se questo panic è terminato
  aborted   bool           // se questo panic è stato interrotto
  goexit    bool
}

La sua struttura è molto simile a defer:

  • link punta alla prossima struttura _panic
  • pc e sp puntano al contesto di esecuzione della funzione chiamante per il recupero futuro
  • arg è il parametro della funzione panic
  • argp punta al parametro di defer; quando si verifica panic, triggera l'esecuzione di defer
  • aborted indica se è stato interrotto

panic, come defer, esiste come lista concatenata nella goroutine:

go
type g struct {
  _panic    *_panic // panic più interno - offset noto a liblink
  _defer    *_defer // defer più interno
}

Panico

Sia che chiamiamo attivamente la funzione panic, sia che il programma verifichi un panic, alla fine si entra nella funzione runtime.gopanic:

go
func gopanic(e any)

All'inizio, controlla prima se il parametro è nil. Se è nil, crea un errore di tipo runtime.PanicNilError:

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

Poi aggiunge il panic corrente alla testa della lista della goroutine:

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

Successivamente entra nel ciclo for per iniziare a elaborare uno per uno la lista defer della goroutine corrente:

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

Se il defer corrente è già stato triggerato da un altro panic, cioè _defer.started == true, allora il panic precedente non verrà eseguito. Poi esegue la funzione corrispondente a defer:

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

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

Dopo l'esecuzione, ricicla la struttura defer corrente e continua con il prossimo defer. Quando tutti i defer sono stati eseguiti e nessuno è stato recuperato, si entra nella funzione runtime.fatalpanic, che è unrecoverable, cioè non recuperabile:

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 // non raggiunto
}

Durante questo processo, printpanics stampa le informazioni sul panic. Le informazioni sullo stack di chiamate che vediamo di solito sono output da questa funzione. Infine, la funzione runtime.exit esce dal programma tramite la system call _ExitProcess.

Recupero

Chiamando la funzione built-in recover, durante la compilazione si trasforma in una chiamata alla funzione 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
}

La sua implementazione è molto semplice, fa solo una cosa: p.recovered = true. Il codice che gestisce effettivamente la logica di recupero è nella funzione gopanic:

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

La logica di recupero è dopo l'esecuzione di defer. A questo punto si capisce perché la funzione recover può essere usata solo in defer. Se si usa recover al di fuori di defer, gp._panic sarebbe nil, e naturalmente p.recovered non verrebbe impostato a true, quindi nella funzione gopanic non si entrerebbe nella logica di recupero.

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

Durante il recupero, si puliscono dalla lista concatenata quei panic che sono stati interrotti. Poi si entra nella funzione runtime.recovery, e runtime.gogo torna al normale flusso logico della funzione utente:

go
func recovery(gp *g) {
  // Informazioni sul defer passate nella struttura 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)
}

Un punto importante da notare è questa riga di codice:

go
gp.sched.ret = 1

Imposta il valore ret a 1. Dai commenti della funzione runtime.deferproc si può vedere:

go
func deferproc(fn func()) {
    ...
  // deferproc restituisce 0 normalmente.
  // una funzione defer che ferma un panic
  // fa restituire 1 a deferproc.
  // il codice generato dal compilatore controlla sempre
  // il valore di ritorno e salta alla
  // fine della funzione se deferproc restituisce != 0.
  return0()
}

Il codice intermedio generato dal compilatore controlla se questo valore è 1. Se lo è, esegue direttamente la funzione runtime.deferreturn. Di solito questa funzione viene eseguita solo prima del ritorno della funzione, il che spiega perché dopo recover la funzione ritorna direttamente.

Golang by www.golangdev.cn edit