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:
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 dereferenceEm 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:
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:
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:
linkaponta para a próxima estrutura_panicpcespapontam para o contexto de execução da função chamadora para facilitar recuperação futuraargé o parâmetro da funçãopanicargpaponta para o parâmetro dodefer; quando ocorrepanic, dispara a execução dodeferabortedindica se foi forçadamente interrompido
panic, assim como defer, também existe na forma de lista encadeada na goroutine:
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:
func gopanic(e any)No início, primeiro detecta se o parâmetro é nil. Se for nil, cria um erro do tipo runtime.PanicNilError:
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:
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:
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:
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:
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:
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:
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.
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:
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 = 1Ela define o valor ret como 1. Pelos comentários da função runtime.deferproc, pode-se ver o seguinte:
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.
