goto es una instrucción de salto incondicional que transfiere el flujo de ejecución a una etiqueta dentro del mismo ámbito de la función. Aunque la programación estructurada y el famoso artículo de Dijkstra, “Go To Statement Considered Harmful”, lo han estigmatizado por su capacidad de crear el llamado “código espagueti”, en la programación de sistemas el goto es una herramienta de precisión.
Su propósito no es crear bucles caóticos, sino gestionar la salida de funciones complejas. Funciona moviendo el puntero de instrucción (IP) directamente a la dirección de memoria asociada a la etiqueta, lo que permite romper la jerarquía natural de los bloques if o while. Se debe utilizar principalmente para centralizar la limpieza de recursos (como free(), fclose() o la liberación de mutexes) cuando una función tiene múltiples puntos de posible error. Si se usa incorrectamente, como intentar saltar entre diferentes funciones o saltar sobre la inicialización de una variable, el compilador lanzará un error o, peor aún, provocarás un comportamiento indefinido (UB).
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
/* Simulación de un recurso de sistema (ej. un mutex) */
typedef struct {
int id;
bool locked;
} Mutex;
/* Función que simula un error al intentar abrir un archivo */
FILE *abrir_archivo(const char *nombre, bool simular_error) {
if (simular_error) {
return NULL;
}
return fopen(nombre, "r");
}
int procesar_datos(const char *nombre_archivo, bool error_en_paso_2) {
int status = 0;
FILE *f = NULL;
char *buffer = NULL;
Mutex *m = malloc(sizeof(Mutex));
if (!m) {
return -1; // Error crítico inmediato: no hay memoria para el mutex
}
m->locked = false;
// Paso 1: Bloquear recurso
printf("[1] Bloqueando mutex...\n");
m->locked = true;
// Paso 2: Asignar memoria
printf("[2] Asignando buffer...\n");
buffer = malloc(1024);
if (!buffer) {
printf("Error en paso 2: Falló malloc\n");
status = -1;
goto err_unlock; // Saltamos directamente a liberar el mutex
}
// Paso 3: Abrir archivo
printf("[3] Abriendo archivo...\n");
f = abrir_archivo(nombre_archivo, error_en_paso_2);
if (!f) {
printf("Error en paso 3: Falló fopen\n");
status = -2;
goto err_free_buffer;
}
// --- Lógica principal (Happy Path) ---
printf("Éxito: Procesando datos de %s...\n", nombre_archivo);
// ... operaciones complejas aquí ...
// Limpieza exitosa
fclose(f);
free(buffer);
m->locked = false;
free(m);
return 0;
/*
* Sección de limpieza centralizada (Error Handling)
* El orden es inverso al de la asignación (LIFO)
*/
err_free_buffer:
free(buffer);
err_unlock:
m->locked = false;
printf("[Cleanup] Mutex liberado y memoria liberada.\n");
free(m);
return status;
}
int main(void) {
printf("--- Intento 1: Éxito ---\n");
procesar_datos("test.txt", false);
printf("\n--- Intento 2: Error en paso 3 ---\n");
procesar_datos("no_existe.txt", true);
// Nota: Para que el ejemplo sea realmente ejecutable sin archivos,
// el error de fopen se disparará si el archivo no existe.
return 0;
}
Desglose del patrón
En el ejemplo anterior, observa cómo la lógica principal (el happy path) permanece plana y legible, sin estar envuelta en una “pirámide del juicio” de condicionales anidados (if (f) { if (buffer) { ... } }).
Cuando abrir_archivo falla, el programa no intenta cerrar un archivo que es NULL ni liberar un buffer que no existe. En su lugar, utiliza goto err_free_buffer. Esta etiqueta le indica al procesador que debe saltar directamente a la sección de limpieza.
Fíjate en la estructura de las etiquetas de error: err_free_buffer y err_unlock. Se organizan en un orden inverso a la adquisición de recursos. Esto es crucial: si asignaste A, luego B, y luego C, en caso de error debes liberar C, luego B y finalmente A. El uso de goto permite que cada error “caiga” hacia la siguiente fase de limpieza necesaria, asegurando que todos los recursos se liberen sin duplicar código de limpieza en cada if.
El error frecuente
Un error sutil pero peligroso en C es intentar saltar sobre la declaración con inicialización de una variable. Aunque el estándar de C es más permisivo que C++, saltar sobre una variable que tiene un valor inicial puede dejar el objeto en un estado indeterminado si el control fluye hacia su uso.
#include <stdio.h>
void error_de_salto(int saltar) {
if (saltar) {
goto etiqueta; // Saltamos sobre la inicialización de 'x'
}
int x = 42; // Declaración con inicialización
printf("Valor de x: %d\n", x);
return;
etiqueta:
// Comportamiento indefinido: 'x' está en scope pero no ha sido inicializada
printf("Valor de x tras el salto: %d\n", x);
}
int main(void) {
error_de_salto(1);
return 0;
}
Si ejecutas este código con gcc -fsanitize=undefined, el compilador o las herramientas como AddressSanitizer te advertirán sobre la lectura de una variable no inicializada. En sistemas críticos, esto se traduce en basura en el registro, valores aleatorios de la pila o, en el peor de los casos, una vulnerabilidad de seguridad que permite leer datos de la memoria que deberían estar protegidos.
N° 34