Skip to content

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.

go
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:

go
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.

go
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:

go
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:

go
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:

go
defer fn1(x, y)

O código após compilação será assim:

go
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:

go
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:

go
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:

go
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 = true

Finalmente, se não encontrar, usa alocação manual. Por fim, pode-se ver este trecho de código:

go
d.heap = true

Isso 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:

go
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.

go
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:

go
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 arg

A assinatura da função deferprocStack é:

go
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:

go
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:

  1. O número de defer na função não pode exceder 8
  2. O produto do número de defer e return não pode exceder 15
  3. defer não pode aparecer em loops
  4. A otimização de compilação não pode ser desativada
  5. Não há chamada manual a os.Exit()
  6. 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:

go
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.

Golang por www.golangdev.cn edit