defer es una instrucción que pospone la ejecución de una función hasta que la función que la contiene retorna o se produce un panic. Su propósito principal es garantizar la limpieza de recursos —como cerrar archivos, liberar sync.Mutex o cerrar conexiones de red— asegurando que la limpieza ocurra independientemente de si la función termina con un return normal o mediante una interrupción inesperada.
Para entender cómo funciona bajo el capó, debemos visualizar una pila (stack) de llamadas. Cuando ejecutas un defer, no estás ejecutando la función, sino que estás registrando una llamada en una pila de ejecución. Por esta razón, el orden de ejecución sigue la lógica LIFO (Last In, First Out): la última instrucción defer declarada es la primera en ejecutarse al salir de la función.
Un detalle crítico que suele causar errores en producción es la evaluación de argumentos. Cuando usas defer f(x), los valores de los argumentos se capturan y se evalúan en el momento exacto en que se lee la línea del defer, no cuando la función se ejecuta realmente. Si necesitas que el defer trabaje con el valor final de una variable al momento de retornar, debes usar un closure (una función anónima), ya que esta captura la referencia a la variable y no su valor instantáneo.
Finalmente, aunque el uso de defer es extremadamente eficiente desde Go 1.14 [disponible desde Go 1.14] gracias a la optimización de open-coded defers (que permite al compilador eliminar el overhead de la pila en casos sencillos), su uso dentro de bucles puede ser desastroso. Los defer no se ejecutan al final de cada iteración del loop, sino al final de la función completa. Esto puede provocar una acumulación masiva de llamadas en la pila, incrementando el consumo de memoria y retrasando la liberación de recursos críticos.
package main
import (
"fmt"
)
func procesarRecursos() {
estado := "inicial"
// 1. Evaluación inmediata de argumentos: se captura el valor actual de 'estado'
defer fmt.Printf("--- [Defer 1] Estado capturado al declarar: %s ---\n", estado)
// 2. Captura por referencia (Closure): captura la referencia a 'estado' para ver el valor final
defer func() {
fmt.Printf("--- [Defer 2] Estado final (vía closure): %s ---\n", estado)
}()
// Cambiamos el estado antes de que los defers se ejecuten
estado = "modificado"
// 3. El problema de los loops: para que el defer se ejecute en cada iteración,
// debemos envolver el proceso en una función anónima inmediata.
for i := 1; i <= 3; i++ {
func(iter int) {
// Este defer se ejecutará al terminar esta función anónima,
// no al terminar 'procesarRecursos'.
defer fmt.Printf(" [Loop] Limpieza de iteración %d\n", iter)
fmt.Printf(" [Loop] Trabajando en %d...\n", iter)
}(i) // Pasamos 'i' como argumento para evitar el problema de captura de variables de loop
}
fmt.Println("Finalizando lógica principal...")
}
func main() {
procesarRecursos()
fmt.Println("Programa completado.")
}
Análisis del código
Al ejecutar procesarRecursos, observa el orden de salida:
- La lógica principal imprime
"Finalizando lógica principal..."y luego la función empieza a “limpiar” hacia atrás en la pila. - El [Defer 2] se ejecuta primero porque fue el último en registrarse. Como es un closure (
func() { ... }), accede directamente a la variableestadoen el ámbito de la función, mostrando"modificado". - El [Defer 1] se ejecuta después. A diferencia del anterior, sus argumentos se evaluaron en la línea donde se declaró. En ese momento,
estadovalía"inicial", por lo que imprime"inicial"aunque la variable haya cambiado después. - En cuanto al bucle, hemos evitado el error común envolviendo el cuerpo del
foren una función anónima:func(iter int) { ... }(i). Esto crea un nuevo marco de pila para cada iteración. Sin esa función anónima, los tresfmt.Printfde limpieza se habrían acumulado y solo se habrían ejecutado al final deprocesarRecursos, después de que la función terminara. - Pasar
icomo argumento a la función anónimafunc(iter int)es una práctica recomendada para asegurar que cada iteración capture su propio valor de índice, evitando que todas las llamadas aldeferapunten a la misma dirección de memoria de la variable de control del bucle.
El error frecuente
Si utilizas defer dentro de un bucle sin una función anónima que delimite el alcance, causarás un “leak” de recursos o un desbordamiento de memoria si el bucle es muy grande.
// ERROR COMÚN: El defer se acumula hasta que la función principal termine
for i := 0; i < 1000; i++ {
f, _ := os.Open("archivo.txt")
defer f.Close() // ¡PELIGRO! No se cierra hasta que procesar() termine
}
En este escenario, si el bucle tiene 1,000 iteraciones, tendrás 1,000 descriptores de archivos abiertos simultáneamente, lo que probablemente agote el límite de archivos abiertos del sistema operativo antes de que la función pueda siquiera empezar a limpiar.
N° 96