Skip to content

panic

panic é uma função integrada em Go. Quando se encontra erros irrecuperáveis, o programa frequentemente lança um panic, como acesso a ponteiro nulo comum:

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

Executar o código acima fará o programa lançar o seguinte panic e então o programa será interrompido:

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

Em algumas situações, chamamos manualmente a função panic para fazer o programa sair, evitando consequências mais graves. Também usamos outra função integrada recover para capturar panic, em conjunto com defer:

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

Por que a função recover deve ser usada dentro de defer, o que a função recover faz - essas questões serão respondidas no conteúdo abaixo.

Estrutura

panic também tem uma estrutura correspondente em tempo de execução, que é runtime._panic. Sua estrutura não é muito complexa:

go
type _panic struct {
  argp      unsafe.Pointer // ponteiro para argumentos da chamada defer executada durante panic; não pode mover - conhecido pela liblink
  arg       any            // argumento para panic
  link      *_panic        // link para panic anterior
  pc        uintptr        // onde retornar em runtime se este panic for ignorado
  sp        unsafe.Pointer // onde retornar em runtime se este panic for ignorado
  recovered bool           // se este panic foi recuperado
  aborted   bool           // se este panic foi abortado
  goexit    bool
}

Sua estrutura é muito similar a defer:

  • link aponta para a próxima estrutura _panic
  • pc e sp apontam para o contexto de execução da função chamadora para facilitar recuperação futura
  • arg é o parâmetro da função panic
  • argp aponta para o parâmetro do defer; quando ocorre panic, dispara a execução do defer
  • aborted indica se foi forçadamente interrompido

panic, assim como defer, também existe na forma de lista encadeada na goroutine:

go
type g struct {
  _panic    *_panic // panic mais interno - offset conhecido pela liblink
  _defer    *_defer // defer mais interno
}

Pânico

Seja chamando ativamente a função panic ou ocorrendo um panic no programa, eventualmente entra-se na função runtime.gopanic:

go
func gopanic(e any)

No início, primeiro detecta se o parâmetro é nil. Se for nil, cria um erro do tipo runtime.PanicNilError:

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

Depois adiciona o panic atual ao cabeçalho da lista da goroutine:

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

Em seguida entra no loop for para começar a processar um por um a lista de defer da goroutine atual:

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 o defer atual já foi disparado por outro panic, ou seja, _defer.started == true, então o panic mais antigo não será executado. Depois executa a função correspondente ao defer:

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

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

Após executar, recicla a estrutura defer atual e continua executando o próximo defer. Quando todos os defer forem executados e não houver recuperação durante o processo, entra-se na função runtime.fatalpanic, que é unrecoverable, ou seja, irrecuperável:

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 // não alcançado
}

Durante este processo, printpanics imprime as informações do panic. As informações de stack trace que normalmente vemos são produzidas por ele. Finalmente, a função runtime.exit sai do programa através da chamada de sistema _ExitProcess.

Recuperação

Ao chamar a função integrada recover, durante a compilação se converte em uma chamada à função 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
}

Sua implementação é muito simples, fazendo apenas uma coisa: p.recovered = true. O código realmente responsável por processar a lógica de recuperação está na função gopanic:

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

A lógica de recuperação está após a execução do defer. Aqui entende-se por que a função recover só pode ser usada em defer. Se recover for usado fora de defer, gp._panic seria nil, naturalmente p.recovered não seria definido como true, e na função gopanic não se entraria na lógica de recuperação.

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 a recuperação, limpa-se da lista aqueles panic que já foram forçadamente interrompidos, depois entra-se na função runtime.recovery, e por runtime.gogo retorna-se ao fluxo lógico normal da função do usuário:

go
func recovery(gp *g) {
  // Informações sobre defer passadas na estrutura 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)
}

Um ponto importante a notar é esta linha de código:

gp.sched.ret = 1

Ela define o valor ret como 1. Pelos comentários da função runtime.deferproc, pode-se ver o seguinte:

go
func deferproc(fn func()) {
    ...
  // deferproc retorna 0 normalmente.
  // uma função defer que interrompe um panic
  // faz deferproc retornar 1.
  // o código que o compilador gera sempre
  // verifica o valor de retorno e pula para o
  // final da função se deferproc retornar != 0.
  return0()
}

O código intermediário gerado pelo compilador verifica se este valor é 1. Se for, executa diretamente a função runtime.deferreturn. Normalmente esta função só é executada antes do retorno da função, o que também explica por que após recover a função retorna diretamente.

Golang por www.golangdev.cn edit