Saat sebuah fungsi mengalokasikan banyak resource—file pointer, buffer memori, socket, lock—dan kemudian gagal di tengah jalan, kita harus membebaskan resource yang sudah dialokasikan sebelum keluar. Tanpa pola yang konsisten, kita mudah lupa: satu cabang error lupa fclose, cabang lain lupa free, dan program akhirnya mengalami resource leak yang sulit dilacak. Pada chapter ini kita akan mempelajari pola goto cleanup—salah satu pemakaian goto yang dianggap sah dan direkomendasikan oleh Linux kernel coding style serta buku-buku referensi C klasik.

Masalah: Banyak Resource, Banyak Cabang Error #

Bayangkan sebuah fungsi yang mengalokasikan tiga resource berturut-turut: buffer memori, array data, dan file handle. Setiap alokasi bisa gagal. Bila kita menulis cleanup di setiap cabang error tanpa pola, kita harus mengulang free/fclose berkali-kali:

c
#include <stdio.h>
#include <stdlib.h>

/* Versi BURUK: tanpa goto, duplikasi cleanup di setiap cabang error.
   Mudah salah - lupa free salah satu resource. */

typedef struct {
    char *nama;
    int  *data;
} context2_t;

int proses_buruk(const char *nama_file, context2_t *out) {
    char *buf = malloc(64);
    if (buf == NULL) {
        return 1;
    }

    int *data = malloc(sizeof(int) * 10);
    if (data == NULL) {
        free(buf);              /* cleanup buf */
        return 2;
    }

    FILE *fp = fopen(nama_file, "r");
    if (fp == NULL) {
        free(buf);              /* cleanup buf */
        free(data);             /* cleanup data */
        return 3;
    }

    if (fgets(buf, 64, fp) == NULL) {
        free(buf);              /* cleanup buf */
        free(data);             /* cleanup data */
        fclose(fp);            /* cleanup fp */
        return 4;
    }

    /* Sukses */
    out->nama = buf;
    out->data = data;
    fclose(fp);
    return 0;
}

int main(void) {
    context2_t ctx = {0};
    int rc = proses_buruk("/tmp/tidak_ada_cleanup2", &ctx);
    printf("proses_buruk -> rc=%d (cleanup duplikatif di setiap cabang)\n", rc);

    if (rc == 0) {
        free(ctx.nama);
        free(ctx.data);
    }
    return 0;
}

Output yang Dihasilkan:

text
proses_buruk -> rc=3 (cleanup duplikatif di setiap cabang)

Versi ini bekerja secara fungsional, tetapi maintenance-nya merepotkan. Setiap kali kita menambah resource baru, kita harus memperbarui setiap cabang error. Bayangkan bila ada 10 resource dan 8 titik kegagalan—kita akan punya 8 blok free/fclose berbeda, masing-masing dengan urutan yang harus dijaga konsisten. Kesalahan kecil seperti lupa fclose di satu cabang akan menyebabkan file descriptor leak yang baru terdeteksi setelah ribuan iterasi.

Pola goto cleanup: Satu Label, Satu Jalur #

Pola yang lebih baik adalah menggunakan goto ke satu label cleanup: di akhir fungsi. Setiap cabang error cukup goto cleanup tanpa perlu mengulang pemanggilan free. Label cleanup memusatkan seluruh pembebasan resource di satu tempat.

c
#include <stdio.h>
#include <stdlib.h>

/* Pola cleanup dengan goto: satu label cleanup, satu jalur keluar.
   Setiap resource di-Free bila tidak NULL. goto di sini SAH dan
   direkomendasikan oleh Linux kernel coding style. */

typedef struct {
    char *nama;
    int  *data;
    FILE *log;
} context_t;

int proses(const char *nama_file, context_t *out) {
    int rc = 0;            /* 0 = sukses, non-zero = kode error */
    char *buf = NULL;
    int *data = NULL;
    FILE *fp = NULL;

    buf = malloc(64);
    if (buf == NULL) { rc = 1; goto cleanup; }

    data = malloc(sizeof(int) * 10);
    if (data == NULL) { rc = 2; goto cleanup; }

    fp = fopen(nama_file, "r");
    if (fp == NULL) { rc = 3; goto cleanup; }

    /* Simulasi baca - kalau file kosong, anggap gagal */
    if (fgets(buf, 64, fp) == NULL) { rc = 4; goto cleanup; }

    /* Sukses: pindahkan ke output */
    out->nama = buf;
    out->data = data;
    out->log  = fp;
    return 0;   /* TIDAK lewat cleanup karena resource dipindahkan */

cleanup:
    /* Setiap resource di-free dengan pengecekan NULL terlebih dahulu.
       urutan: yang dialokasikan TERAKHIR di-free DULU (LIFO). */
    if (fp)   fclose(fp);
    if (data) free(data);
    if (buf)  free(buf);
    return rc;
}

int main(void) {
    context_t ctx = {0};

    /* Coba dengan file tidak ada */
    int rc = proses("/tmp/file_cleanup_tidak_ada", &ctx);
    printf("proses(file_tidak_ada) -> rc=%d\n", rc);

    /* Coba dengan file kosong */
    FILE *kosong = fopen("/tmp/file_cleanup_kosong", "w");
    if (kosong) { fclose(kosong); }   /* buat file kosong */
    rc = proses("/tmp/file_cleanup_kosong", &ctx);
    printf("proses(file_kosong)    -> rc=%d\n", rc);

    /* Coba dengan file berisi */
    FILE *ada = fopen("/tmp/file_cleanup_berisi", "w");
    if (ada) { fprintf(ada, "Halo, ini isi file\n"); fclose(ada); }
    rc = proses("/tmp/file_cleanup_berisi", &ctx);
    printf("proses(file_berisi)    -> rc=%d\n", rc);

    if (rc == 0) {
        printf("  nama='%s'\n", ctx.nama);
        free(ctx.nama);
        free(ctx.data);
        fclose(ctx.log);
    }
    return 0;
}

Output yang Dihasilkan:

text
proses(file_tidak_ada) -> rc=3
proses(file_kosong)    -> rc=4
proses(file_berisi)    -> rc=0
  nama='Halo, ini isi file
'

Tiga skenario uji menghasilkan tiga kode berbeda (3, 4, 0). Untuk rc=3 (file tidak ada), hanya buf yang perlu di-free—data dan fp belum dialokasikan saat error terjadi. Untuk rc=4 (file kosong), ketiga resource sudah dialokasikan dan harus dibebaskan. Untuk rc=0 (sukses), resource dipindahkan ke output struct dan tidak dibebaskan—pemanggil yang bertanggung jawab.

Mari kita telaah baris-baris kode di atas:

  1. Baris 16-18 (char *buf = NULL; int *data = NULL; FILE *fp = NULL;): SEMUA resource diinisialisasi ke NULL. Inilah kunci pola goto cleanup—free(NULL) dan fclose(NULL) tidak sama. free(NULL) aman (tidak melakukan apa-apa menurut standar C §7.22.3.3), tetapi fclose(NULL) adalah undefined behavior. Karena itu kita pakai if (fp) sebelum fclose.
  2. Baris 20-21 (buf = malloc(64); if (buf == NULL) { rc = 1; goto cleanup; }): alokasi pertama. Bila gagal, set rc lalu lompat ke cleanup. Di cleanup, if (fp) false (fp masih NULL), if (data) false (data masih NULL), if (buf) false (buf juga NULL karena malloc gagal). Tidak ada yang dibebaskan—tepat.
  3. Baris 23-24 (data = malloc(...); if (data == NULL) { rc = 2; goto cleanup; }): alokasi kedua. Bila gagal, buf sudah dialokasikan dan harus dibebaskan. goto cleanup akan menjalankan if (buf) free(buf)—buf non-NULL jadi dibebaskan.
  4. Baris 26-27 (fp = fopen(...); if (fp == NULL) { rc = 3; goto cleanup; }): alokasi ketiga. buf dan data sudah ada. goto cleanup membebaskan keduanya.
  5. Baris 30 (if (fgets(buf, 64, fp) == NULL) { rc = 4; goto cleanup; }): kegagalan logis—file berhasil dibuka tetapi kosong. Sekali lagi goto cleanup menangani ketiga resource.
  6. Baris 33-36 (out->nama = buf; ... return 0;): jalur sukses. Resource dipindahkan ke output struct dan fungsi langsung return 0—TIDAK lewat cleanup karena kita ingin resource tetap hidup untuk pemanggil.
  7. Baris 38-43 (cleanup: if (fp) fclose(fp); if (data) free(data); if (buf) free(buf);): label cleanup. Pembebasan dilakukan dalam urutan LIFO (terakhir dialokasikan pertama dibebaskan). Setiap resource diperiksa != NULL sebelum dibebaskan.

Skenario Realistis: Parser Konfigurasi #

Mari kita terapkan pola ini di skenario yang lebih realistis: membaca file konfigurasi baris-per-baris, memparse angka, menyimpan ke array yang di-resize secara dinamis. Setiap langkah punya titik kegagalan sendiri.

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Skenario realistis: baca konfigurasi dari file, alokasikan buffer,
   parse ke array integer. Banyak titik kegagalan. */

typedef struct {
    int *nilai;
    size_t jumlah;
} hasil_parse_t;

int parse_konfigurasi(const char *path, hasil_parse_t *out) {
    int rc = 0;
    FILE *fp = NULL;
    char *line = NULL;
    int *arr = NULL;
    size_t kapasitas = 4;

    fp = fopen(path, "r");
    if (fp == NULL) { rc = 1; goto cleanup; }

    line = malloc(256);
    if (line == NULL) { rc = 2; goto cleanup; }

    arr = malloc(sizeof(int) * kapasitas);
    if (arr == NULL) { rc = 3; goto cleanup; }

    size_t n = 0;
    while (fgets(line, 256, fp) != NULL) {
        /* Abaikan baris kosong dan komentar */
        if (line[0] == '\n' || line[0] == '#') continue;

        /* Parse angka */
        int val;
        if (sscanf(line, "%d", &val) != 1) {
            fprintf(stderr, "Baris tidak bisa di-parse: %s", line);
            rc = 4;
            goto cleanup;
        }

        /* Resize bila perlu */
        if (n == kapasitas) {
            size_t baru = kapasitas * 2;
            int *tmp = realloc(arr, sizeof(int) * baru);
            if (tmp == NULL) { rc = 5; goto cleanup; }
            arr = tmp;
            kapasitas = baru;
        }
        arr[n++] = val;
    }

    out->nilai = arr;
    out->jumlah = n;
    /* Sukses: kepemilikan arr dipindah ke out, jangan di-free */
    free(line);
    fclose(fp);
    return 0;

cleanup:
    /* LIFO: bebas resource dalam urutan terakhir-dialokasikan-dulu */
    if (arr)  free(arr);
    if (line) free(line);
    if (fp)   fclose(fp);
    return rc;
}

int main(void) {
    /* Buat file konfigurasi demo */
    FILE *cfg = fopen("/tmp/cfg_demo.txt", "w");
    if (cfg) {
        fprintf(cfg, "# Konfigurasi contoh\n");
        fprintf(cfg, "10\n");
        fprintf(cfg, "20\n");
        fprintf(cfg, "30\n");
        fprintf(cfg, "\n");          /* baris kosong */
        fprintf(cfg, "40\n");
        fclose(cfg);
    }

    hasil_parse_t hasil = {0};
    int rc = parse_konfigurasi("/tmp/cfg_demo.txt", &hasil);

    printf("parse_konfigurasi('/tmp/cfg_demo.txt') -> rc=%d\n", rc);
    if (rc == 0) {
        printf("jumlah=%zu, nilai:", hasil.jumlah);
        for (size_t i = 0; i < hasil.jumlah; i++) {
            printf(" %d", hasil.nilai[i]);
        }
        printf("\n");
        free(hasil.nilai);
    }

    /* Test dengan file tidak valid (string bukan angka) */
    FILE *bad = fopen("/tmp/cfg_bad.txt", "w");
    if (bad) { fprintf(bad, "10\nbukan_angka\n"); fclose(bad); }
    rc = parse_konfigurasi("/tmp/cfg_bad.txt", &hasil);
    printf("\nparse_konfigurasi('/tmp/cfg_bad.txt') -> rc=%d (diharapkan 4)\n", rc);

    return 0;
}

Output yang Dihasilkan:

text
parse_konfigurasi('/tmp/cfg_demo.txt') -> rc=0
jumlah=4, nilai: 10 20 30 40

parse_konfigurasi('/tmp/cfg_bad.txt') -> rc=4 (diharapkan 4)

Stderr:

text
Baris tidak bisa di-parse: bukan_angka

parse_konfigurasi menggabungkan banyak teknik sekaligus: alokasi file, buffer line, array dinamis dengan realloc, dan parsing baris per baris. Lima titik kegagalan (rc=1 sampai rc=5) semuanya diarahkan ke label cleanup yang sama. Bila sscanf gagal di tengah loop (kasus cfg_bad.txt), array arr yang sudah berisi 10 dibebaskan dengan benar—tidak ada memory leak.