Skip to content

defer

defer es una palabra clave que aparece con mucha frecuencia en el desarrollo diario con Go. Ejecuta las funciones asociadas con defer en orden LIFO (último en entrar, primero en salir). Muchas veces utilizamos este mecanismo para operaciones de liberación de recursos, como el cierre de archivos.

go
fd, err := os.Open("/dev/stdin")
if err != nil{
    return err
}
defer fd.Close()
...

La aparición tan frecuente de esta palabra clave hace que sea necesario entender la estructura detrás de ella.

Estructura

La palabra clave defer corresponde a la estructura runtime._defer. Su estructura no es compleja:

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp at time of defer
  pc        uintptr // pc at time of defer
  fn        func()  // can be nil for open-coded defers
  _panic    *_panic // panic that is running defer
  link      *_defer // next defer on G; can point to either heap or stack!
  fd   unsafe.Pointer // funcdata for the function associated with the frame
  varp uintptr        // value of varp for the stack frame
  framepc uintptr
}

El campo fn es la función correspondiente a la palabra clave defer, link representa el siguiente defer enlazado, sp y pc registran la información de la función llamadora, utilizada para determinar a qué función pertenece el defer. defer existe como una lista enlazada en tiempo de ejecución, la cabeza de la lista está en la goroutine G, por lo que defer está directamente asociado con la goroutine.

go
type g struct {
    ...
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
    ...
}

Cuando la goroutine ejecuta una función, los defer de la función se agregan secuencialmente desde la cabeza de la lista enlazada:

go
defer fn1()
defer fn2()
defer fn3()

Ese código corresponde a esta figura:

Además de la goroutine, P también tiene cierta relación con defer. En la estructura de P, hay un campo deferpool, como se muestra:

go
type p struct {
  ...
  deferpool    []*_defer // pool of available defer structs (see panic.go)
  deferpoolbuf [32]*_defer
    ...
}

En deferpool se almacenan estructuras defer pre-asignadas, utilizadas para asignar nuevas estructuras defer a la goroutine G asociada con P, lo que puede reducir la sobrecarga.

Asignación

En la sintaxis, el uso de la palabra clave defer será convertido por el compilador en una llamada a la función runtime.deferproc. Por ejemplo, el código Go se escribe así:

go
defer fn1(x, y)

Pero después de la compilación, el código real es así:

go
deferproc(func(){
  fn1(x, y)
})

Por lo tanto, la función pasada a defer realmente no tiene parámetros ni valores de retorno. El código de la función deferproc es el siguiente:

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 función es responsable de crear la estructura defer y agregarla a la cabeza de la lista enlazada de la goroutine G. La función runtime.newdefer intentará obtener la estructura defer pre-asignada desde deferpool en 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)
}

Primero llena la mitad de deferpool local desde sched.deferpool global, luego intenta obtener desde deferpool en 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 {
    // Allocate new defer.
    d = new(_defer)
}
d.heap = true

Finalmente, si no se puede encontrar, se usará la asignación manual. Finalmente se puede ver este código:

go
d.heap = true

Esto indica que defer se asigna en el heap. Correspondientemente, cuando es false, se asignará en el stack. La memoria asignada en el stack se reciclará automáticamente al retornar, y su eficiencia de gestión de memoria es mayor que en el heap. El factor que decide si se asigna en el stack es el número de niveles de bucle. Esta parte de la lógica se puede rastrear hasta el método escape.goDeferStmt en 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 el número de niveles de bucle de la sentencia actual. Si la sentencia defer actual no está en un bucle, se asignará en el 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)
    }

Si se asigna en el stack, se creará directamente la estructura defer en el stack, y finalmente la función runtime.deferprocStack completará la creación de la estructura 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))
    call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
    call.AddArgs(callArgs...)
    call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer arg

La signatura de la función deferprocStack es:

go
func deferprocStack(d *_defer)

Su lógica de creación específica no es muy diferente de deferproc. La principal diferencia es que al asignar en el stack, el origen de la estructura defer es una estructura creada directamente, mientras que el origen de defer asignado en el heap es la función new.

Ejecución

Cuando la función está a punto de retornar o ocurre un panic, se entra en la función runtime.deferreturn, que es responsable de tomar defer de la lista enlazada de la goroutine y ejecutarlo.

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

Primero se obtiene el stack frame de la función actual mediante getcallersp() y se compara con sp en la estructura defer para determinar si defer pertenece a la función actual. Luego se toma la estructura defer de la cabeza de la lista enlazada, y se usa gp._defer = d.link para ejecutar el siguiente defer. Después, mediante la función runtime.freedefer, se libera la estructura defer de vuelta al pool. Finalmente se llama a fn para ejecutar. Esto continúa en bucle hasta que se ejecuten todos los defer pertenecientes a la función actual.

Open-Coded

El uso de defer no está libre de costos. Aunque proporciona conveniencia en la sintaxis, no es una llamada directa a función, sino que pasa por una serie de procesos, por lo que causará pérdida de rendimiento. Por lo tanto, más tarde los oficiales de Go diseñaron una optimización: open-coded. Es una forma de optimización para defer. Su nombre original en inglés es open-coded, y en China generalmente se traduce como 开放编码 (código abierto). Aquí open significa expandir, es decir, expandir el código de la función defer en el código de la función actual, similar a la inline de funciones. Esta optimización tiene las siguientes restricciones:

  1. El número de defer en la función no puede exceder 8
  2. El producto del número de defer y return no puede exceder 15
  3. defer no puede aparecer en bucles
  4. La optimización de compilación no está deshabilitada
  5. No hay llamada manual a os.Exit()
  6. No es necesario copiar parámetros desde el heap

Esta parte de la lógica de juicio se puede rastrear hasta la función 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
}

Luego Go creará una variable entera de 8 bits deferBits en la función actual para usar como bitmap para marcar defer. Cada bit marca uno. Un entero uint8 de 8 bits puede representar como máximo 8. Si el bit correspondiente es 1, el defer optimizado con open-coded correspondiente se ejecutará cuando la función vaya a retornar.

Golang editado por www.golangdev.cn