Menu

defer en Golang: orden, evaluación de argumentos y trampas

defer programa una llamada para que se ejecute cuando retorna la función que la contiene. Aprende el orden LIFO, cuándo se evalúan los argumentos, cerrar archivos y liberar mutexes, defer en bucles y modificar resultados con nombre.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Qué hace defer

defer añade una llamada a función a una lista. Cuando la función que la contiene retorna, la lista se ejecuta en orden inverso.

Salida:

start
end
deferred 3
deferred 2
deferred 1

El orden es último en entrar, primero en salir, como una pila. Coincide con cómo se anidan los recursos: si abres A y luego B, normalmente quieres cerrar B antes que A.

La limpieza junto a la adquisición

El uso principal de defer es escribir la limpieza en la línea justo después de la adquisición, para que ninguna ruta de retorno pueda olvidarla.

Fíjate en el orden: primero comprueba el error y después difiere. Si os.Open falló, f es nil, y diferir f.Close() antes de la comprobación llamaría a Close sobre un *os.File nil (que devuelve un error que nunca ves y confunde a quien lee el código).

La misma forma sirve para los locks:

mu.Lock()
defer mu.Unlock()

Si el código entre ambos provoca un panic, el mutex se libera igualmente.

Los argumentos se evalúan de inmediato

La función diferida y sus argumentos se evalúan cuando se ejecuta la sentencia defer. Solo la llamada en sí espera.

Salida:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) capturó el valor 1 en la línea del defer. La closure no tiene argumentos; lee x cuando por fin se ejecuta. Elige la forma que coincida con lo que quieres registrar.

Esto también se aplica a los receptores de métodos. defer t.Stop() evalúa t en ese momento, así que reasignar t después no cambia qué valor se detiene.

Un truco habitual de medición de tiempos usa esta regla a propósito:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") se llama de inmediato (puede imprimir "enter" y anotar la hora de inicio), y lo que se difiere es la función que devuelve.

defer en un bucle

Las llamadas diferidas se ejecutan cuando la función retorna, no al final de cada iteración del bucle. En un bucle sobre muchos archivos, esto mantiene todos los archivos abiertos hasta que termina la función.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

Con miles de rutas, esto agota los descriptores de archivo. Mueve el cuerpo a su propia función para que cada defer se ejecute en cada iteración:

Cada llamada a processFile cierra su archivo antes de que se abra el siguiente. Un literal de función llamado en el sitio (func() { ... }()) funciona igual cuando un helper con nombre parece excesivo.

Modificar los valores de retorno

Una closure diferida se ejecuta después de que la sentencia return haya asignado los resultados, y puede modificar los resultados con nombre antes de que los vea quien llama.

Esto imprime 10 y save failed: disk full. Con un resultado sin nombre, una función diferida se sigue ejecutando, pero no tiene forma de cambiar lo que se devuelve.

Capturar el error de Close

defer f.Close() descarta el error de Close. Para archivos que solo lees, no pasa nada. Para archivos en los que escribiste, Close puede informar de un volcado final fallido, así que el error importa. Un resultado con nombre te permite conservarlo:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) es una alternativa más corta cuando quieres informar de los dos errores.

defer, panic y recover

Las llamadas diferidas se ejecutan mientras un panic deshace la pila. Es el único sitio donde recover hace algo, y es como un servidor evita que una petición defectuosa tumbe el proceso. Los detalles están en panic y recover.

Las llamadas diferidas no se ejecutan cuando el programa sale mediante os.Exit o log.Fatal. Si main difiere una limpieza y luego llama a os.Exit(1), la limpieza se salta.

Coste

Desde Go 1.14, el compilador genera en línea (open-coded) la mayoría de los defers, y cuestan unos pocos nanosegundos. Usar defer para cada liberación de mutex y cada cierre de archivo es el estilo normal. Los defers dentro de bucles son la excepción: no se pueden generar en línea y recurren a una ruta más lenta, otra razón más para mover el cuerpo de los bucles a funciones.

Errores comunes

  • Diferir antes de comprobar el error. Comprueba primero el err de Open y después haz defer Close.
  • Esperar una limpieza por iteración en un bucle. Los defers se ejecutan al salir de la función.
  • Esperar que un argumento diferido vea cambios posteriores. Los argumentos quedan fijados en la línea del defer. Usa una closure para leer al salir.
  • Confiar en defer con os.Exit. Nunca se ejecuta.

Preguntas frecuentes

¿Qué hace defer en Go?

defer f() programa f() para que se ejecute cuando retorne la función que la contiene, ya sea normalmente, con un return anticipado o por un panic. Se usa para poner la limpieza (cerrar un archivo, liberar un mutex) justo al lado del código que obtuvo el recurso.

¿En qué orden se ejecutan las llamadas diferidas en Go?

Último en entrar, primero en salir. La llamada diferida más reciente se ejecuta primero. defer fmt.Println(1); defer fmt.Println(2) imprime 2 y luego 1.

¿Cuándo se evalúan los argumentos de una función diferida?

De inmediato, cuando se ejecuta la sentencia defer, no cuando se ejecuta la llamada. x := 1; defer fmt.Println(x); x = 2 imprime 1. Para leer el valor en el momento de salir, difiere una closure: defer func() { fmt.Println(x) }().

¿Se ejecuta defer con un panic o con os.Exit?

Las llamadas diferidas se ejecutan mientras un panic deshace la pila, y por eso recover funciona dentro de ellas. No se ejecutan cuando el programa llama a os.Exit (o a log.Fatal, que lo llama), y no se ejecutan en otras goroutines cuando main retorna.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR