defer
defer é uma palavra-chave que aparece com muita frequência no desenvolvimento diário em Go. Ela executa as funções associadas ao defer em ordem LIFO (último a entrar, primeiro a sair). Muitas vezes utilizamos esse mecanismo para operações de liberação de recursos, como fechamento de arquivos.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...A alta frequência de uso desta palavra-chave torna necessário entendermos sua estrutura interna.
Estrutura
A palavra-chave defer corresponde à estrutura runtime._defer em tempo de execução. Sua estrutura não é muito complexa:
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp no momento do defer
pc uintptr // pc no momento do defer
fn func() // pode ser nil para defers de código aberto
_panic *_panic // panic que está executando defer
link *_defer // próximo defer em G; pode apontar para heap ou stack!
fd unsafe.Pointer // funcdata para a função associada ao frame
varp uintptr // valor de varp para o frame de stack
framepc uintptr
}O campo fn é a função correspondente à palavra-chave defer, link representa o próximo defer encadeado, sp e pc registram informações da função chamadora, usadas para julgar a qual função o defer pertence. defer existe na forma de lista encadeada em tempo de execução, onde o cabeçalho da lista está na goroutine G, então defer está diretamente associado à goroutine.
type g struct {
...
_panic *_panic // panic mais interno - offset conhecido pela liblink
_defer *_defer // defer mais interno
...
}Quando a goroutine executa uma função, os defer da função são adicionados sequencialmente ao cabeçalho da lista:
defer fn1()
defer fn2()
defer fn3()O código acima corresponde à figura abaixo:

Além da goroutine, P também tem certa relação com defer. Na estrutura de P, há um campo deferpool, conforme mostrado abaixo:
type p struct {
...
deferpool []*_defer // pool de estruturas defer disponíveis (veja panic.go)
deferpoolbuf [32]*_defer
...
}deferpool armazena estruturas defer pré-alocadas, usadas para alocar novas estruturas defer para a goroutine G associada ao P, reduzindo a sobrecarga.
Alocação
Sintaticamente, ao usar a palavra-chave defer, o compilador a converte em uma chamada à função runtime.deferproc. Por exemplo, se o código Go for escrito assim:
defer fn1(x, y)O código após compilação será assim:
deferproc(func(){
fn1(x, y)
})Portanto, na verdade, a função passada para defer não tem parâmetros nem valores de retorno. A função deferproc é conforme o código abaixo:
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}Esta função é responsável por criar a estrutura defer e adicioná-la ao cabeçalho da lista da goroutine G. A função runtime.newdefer tenta obter a estrutura defer pré-alocada do deferpool em P:
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}Primeiro, preenche metade do deferpool local a partir do sched.deferpool global, depois tenta obter do deferpool em P:
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Aloca novo defer.
d = new(_defer)
}
d.heap = trueFinalmente, se não encontrar, usa alocação manual. Por fim, pode-se ver este trecho de código:
d.heap = trueIsso indica que defer foi alocado no heap. Quando for false, será alocado na stack. A memória alocada na stack é automaticamente reciclada ao retornar, e sua eficiência de gerenciamento de memória é maior do que no heap. O fator que decide se será alocado na stack é o número de níveis de loop. Esta parte da lógica pode ser rastreada até o método escape.goDeferStmt em cmd/compile/ssagen:
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth representa o número de níveis de loop da instrução atual. Se a instrução defer não estiver em um loop, será alocada na stack.
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}Se for alocado na stack, a estrutura defer é criada diretamente na stack, e finalmente a função runtime.deferprocStack completa a criação da estrutura defer:
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer argA assinatura da função deferprocStack é:
func deferprocStack(d *_defer)A lógica específica de criação não é muito diferente de deferproc. A principal diferença é que, ao alocar na stack, a origem da estrutura defer é uma estrutura criada diretamente, enquanto a origem do defer alocado no heap é a função new.
Execução
Quando a função está prestes a retornar ou ocorre um panic, entra-se na função runtime.deferreturn, responsável por retirar defer da lista da goroutine e executá-lo:
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}Primeiro, obtém-se o frame de stack da função atual através de getcallersp() e compara-se com o sp na estrutura defer para julgar se o defer pertence à função atual. Depois, retira-se a estrutura defer do cabeçalho da lista e executa-se o próximo defer com gp._defer = d.link. Em seguida, libera-se a estrutura defer de volta ao pool através da função runtime.freedefer, e finalmente chama-se fn para execução. Continua-se em loop até executar todos os defer pertencentes à função atual.
Open-coded
O uso de defer não é sem custo. Embora forneça conveniência sintática, não é uma chamada de função direta, passando por uma série de processos, o que causa perda de desempenho. Por isso, mais tarde os desenvolvedores do Go projetaram uma otimização chamada open-coded. É uma forma de otimização para defer, cujo nome original em inglês é open-coded, e domesticamente é traduzido como "código aberto" (open-coded). O "open" aqui significa expandir, ou seja, expandir o código da função defer no código da função atual, como inline de função. Esta otimização tem as seguintes restrições:
- O número de
deferna função não pode exceder 8 - O produto do número de
deferereturnnão pode exceder 15 defernão pode aparecer em loops- A otimização de compilação não pode ser desativada
- Não há chamada manual a
os.Exit() - Não é necessário copiar parâmetros do heap
Esta parte da lógica de julgamento pode ser rastreada até esta parte do código na função cmd/compile/ssagen.buildssa:
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}Depois, Go cria uma variável inteira de 8 bits deferBits na função atual para servir como bitmap para marcar defer. Cada bit marca um defer. Um inteiro de 8 bits uint8 pode representar no máximo 8 defer. Se o bit correspondente for 1, o defer otimizado com open-coded será executado quando a função retornar.
