defer en Go: cuándo se ejecuta, orden LIFO y usos prácticos

defer pospone la ejecución de una llamada a función hasta que la función que la contiene retorna. Se ejecuta aunque la función retorne normalmente, con un return anticipado o debido a un pánico. Es el mecanismo idiomático de Go para el trabajo de limpieza: cerrar ficheros, liberar mutex, cerrar conexiones de base de datos o registrar el tiempo de ejecución de una función.

Comportamiento básico

package main

import "fmt"

func main() {
    fmt.Println("inicio")
    defer fmt.Println("diferido")
    fmt.Println("fin")
}
// inicio
// fin
// diferido

El deferred se ejecuta justo antes de que main retorne, independientemente de lo que haya ocurrido dentro de la función.

Cuándo se evalúan los argumentos

Los argumentos de la función diferida se evalúan en el momento en que se declara el defer, no cuando se ejecuta:

func main() {
    x := 10
    defer fmt.Println("valor de x:", x) // captura x=10 ahora
    x = 99
    fmt.Println("x ahora es:", x)
}
// x ahora es: 99
// valor de x: 10

Orden LIFO con múltiples defers

Cuando hay varios defers en una misma función, se ejecutan en orden inverso (último en declararse, primero en ejecutarse):

func main() {
    defer fmt.Println("primero en declarar, último en ejecutar")
    defer fmt.Println("segundo")
    defer fmt.Println("tercero — se ejecuta primero")
}
// tercero — se ejecuta primero
// segundo
// primero en declarar, último en ejecutar

Cerrar ficheros con defer

El uso más habitual es garantizar que un recurso se cierre independientemente de cómo acabe la función:

func procesarFichero(ruta string) error {
    f, err := os.Open(ruta)
    if err != nil {
        return fmt.Errorf("abrir %s: %w", ruta, err)
    }
    defer f.Close() // se ejecutará cuando procesarFichero retorne

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    return scanner.Err()
}

Sin defer, habría que llamar a f.Close() en cada punto de salida de la función, y es fácil olvidarlo.

defer y recover

defer es la única forma de ejecutar recover(), que intercepta un pánico y permite continuar la ejecución:

func seguro(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("pánico recuperado: %v", r)
        }
    }()
    f()
    return nil
}

func main() {
    err := seguro(func() {
        panic("algo salió muy mal")
    })
    fmt.Println(err) // pánico recuperado: algo salió muy mal
}

La trampa del defer en bucles

Poner un defer dentro de un bucle no cierra el recurso en cada iteración, sino cuando la función que contiene el bucle retorna. Si abres muchos ficheros en un bucle, podrías agotar los descriptores de fichero:

// MAL: los Close se acumulan hasta que la función retorna
for _, ruta := range rutas {
    f, _ := os.Open(ruta)
    defer f.Close()
    procesar(f)
}

// BIEN: extrae a una función auxiliar
for _, ruta := range rutas {
    procesarFichero(ruta)
}

func procesarFichero(ruta string) {
    f, _ := os.Open(ruta)
    defer f.Close() // se cierra al final de esta función, no del bucle
    procesar(f)
}

Medir el tiempo de ejecución

func medir(nombre string) func() {
    inicio := time.Now()
    return func() {
        fmt.Printf("%s tardó %vn", nombre, time.Since(inicio))
    }
}

func tarea() {
    defer medir("tarea")()
    time.Sleep(100 * time.Millisecond)
}

func main() {
    tarea()
    // tarea tardó 100.2ms
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP