La sentencia defer en Zig es una forma de ejecutar código automáticamente al salir del bloque actual. Cuando trabajamos en lenguajes de sistemas, la gestión de recursos es crítica: si abres un archivo, debes cerrarlo; si pides memoria, debes liberarla.
En lenguajes como C, el código de limpieza suele quedar lejos de la adquisición del recurso, a menudo al final de la función. Con varios puntos de salida es fácil olvidar alguna ruta y provocar una fuga.
Zig incorpora para este caso la sentencia defer.
La idea es sencilla: “ejecuta este código cuando el flujo salga del bloque actual”. Incluye salidas mediante return, break o propagación de errores, pero no garantiza una limpieza ante la finalización abrupta del proceso.
Sintaxis básica
Usamos la palabra clave defer seguida de una expresión o un bloque de código. Zig programará esa ejecución para el momento exacto en que el flujo de ejecución abandone las llaves {} actuales.
pub fn ejemplo() void {
std.debug.print("Inicio\n", .{});
// Esto se ejecutará AL FINAL de la función
defer std.debug.print("Fin (ejecutado por defer)\n", .{});
std.debug.print("Haciendo cosas...\n", .{});
}Salida:
Inicio
Haciendo cosas...
Fin (ejecutado por defer)Así podemos colocar la limpieza inmediatamente después de adquirir el recurso. Visualmente, el “abrir” y el “cerrar” quedan juntos y el código resulta más fácil de revisar.
El orden importa: LIFO
¿Qué pasa si tenemos múltiples defer en el mismo bloque? Zig los ejecuta en orden inverso a su declaración (Last In, First Out - LIFO).
Esto tiene todo el sentido del mundo si pensamos en dependencias. Si creas A y luego creas B (que depende de A), necesitas destruir B antes de destruir A.
{
defer std.debug.print("1. Limpiando A\n", .{});
defer std.debug.print("2. Limpiando B\n", .{});
defer std.debug.print("3. Limpiando C\n", .{});
std.debug.print("Trabajando...\n", .{});
}Salida:
Trabajando...
3. Limpiando C
2. Limpiando B
1. Limpiando Adefer y gestión de errores
La utilidad más clara de defer aparece cuando empezamos a manejar errores y retornos tempranos.
Supongamos que una función tiene que realizar tres pasos. Si el paso 2 falla, el paso 1 debe limpiarse. Si el paso 3 falla, el 1 y el 2 deben limpiarse. En C esto es un infierno de goto o if anidados.
En Zig, es lineal y limpio:
fn procesarDatos() !void {
const recurso1 = crearRecurso1();
defer destruir(recurso1); // Se limpiará pase lo que pase
// Si esto falla y retorna error, recurso1 se limpia automáticamente
try pasoPeligroso();
const recurso2 = crearRecurso2();
defer destruir(recurso2); // Ahora se limpiarán 2 y luego 1
try otroPasoPeligroso();
}Si otroPasoPeligroso devuelve un error, Zig ejecutará destruir(recurso2) y luego destruir(recurso1) antes de propagarlo al llamante.
Diferencia con Go: ámbito de bloque
Si vienes de Go, ten en cuenta esta diferencia:
- En Go, el
deferse ejecuta al final de la función. - En Zig, el
deferse ejecuta al final del bloque (scope).
Esto permite controlar con precisión los recursos adquiridos dentro de bucles o bloques internos.
// Ejemplo: Defer dentro de un bucle
for (archivos) |archivo| {
const handle = abrir(archivo);
// En Zig, esto cierra el archivo AL FINAL DE CADA ITERACIÓN
// En Go, se acumularían abiertos hasta salir de la función (peligroso)
defer cerrar(handle);
procesar(handle);
}Gracias al scope de bloque, no agotamos los descriptores de archivo ni la memoria en bucles largos.
Casos de uso comunes
Gestión de memoria
Es el uso más habitual. Pides memoria y pones el defer inmediatamente.
const memoria = try allocator.alloc(u8, 100);
defer allocator.free(memoria);Archivos y sockets
const file = try dir.openFile("data.txt", .{});
defer file.close();Mutex y concurrencia
Evita que se te olvide desbloquear un semáforo y provoques un deadlock.
mutex.lock();
defer mutex.unlock();
// Haz lo que quieras, incluso return con error, el mutex se liberará.
hacerCosasCriticas();Fugas de recursos
Aunque defer ayuda enormemente, no lo hace solo. Solo limpia lo que tú le digas que limpie.
Si tienes una estructura de datos compleja (como un árbol o una lista enlazada) y solo haces defer allocator.free(raiz), solo liberarás el nodo raíz. Eres responsable de escribir una función deinit() que recorra y limpie todo, y luego llamar a defer arbol.deinit().
Muchos tipos de la biblioteca estándar tienen un método deinit. Su firma depende del tipo y puede requerir el allocator, por ejemplo defer lista.deinit(allocator).