BAB 31: Defer dan Exit
Pahami defer untuk eksekusi kode yang dijamin berjalan saat fungsi selesai, dan os.Exit untuk menghentikan program secara paksa.
Sistem pemantau yang kamu bangun di bab sebelumnya memiliki satu celah yang tidak terlihat: bagaimana memastikan resource seperti koneksi atau file selalu ditutup, bahkan ketika fungsi keluar lebih awal karena timeout atau error? Menempatkan kode pembersihan di akhir fungsi tidak cukup — Go bisa meninggalkan fungsi dari berbagai titik melalui return. Di sinilah defer hadir sebagai solusi yang elegan.
Apa Itu Defer
defer adalah kata kunci Go yang menunda eksekusi sebuah statement hingga fungsi yang memanggilnya selesai — entah karena mencapai akhir fungsi, karena return, atau karena panic. Kode yang di-defer dijamin akan berjalan sebelum fungsi benar-benar keluar.
// audit.go
package main
import "fmt"
func bukaAuditLog(nama string) {
fmt.Printf("membuka audit log: %s\n", nama)
defer fmt.Printf("menutup audit log: %s\n", nama) // dieksekusi terakhir
fmt.Println("mencatat aktivitas pengguna")
fmt.Println("mencatat perubahan data")
// defer berjalan di sini, setelah semua statement di atas
}
func main() {
bukaAuditLog("sistem-produksi")
}
membuka audit log: sistem-produksi
mencatat aktivitas pengguna
mencatat perubahan data
menutup audit log: sistem-produksi
Posisi defer dalam kode tidak mencerminkan urutan eksekusinya. Meskipun ditulis di baris kedua fungsi, ia baru berjalan paling akhir — setelah semua statement lain selesai.
Defer Berjalan Sebelum Return
Salah satu keunggulan defer adalah ia berjalan meski fungsi keluar lebih awal lewat return. Ini yang membuat defer sangat andal untuk cleanup — kamu tidak perlu memikirkan semua jalur keluar fungsi secara manual.
// audit.go
package main
import "fmt"
func validasiAkses(userID int) string {
defer fmt.Println("validasi selesai, koneksi dilepas")
if userID <= 0 {
fmt.Println("user ID tidak valid, batalkan")
return "ditolak" // defer tetap berjalan sebelum return ini
}
if userID > 9999 {
fmt.Println("user ID di luar rentang")
return "tidak ditemukan" // defer juga berjalan sebelum ini
}
fmt.Println("akses diberikan")
return "diizinkan"
}
func main() {
hasil := validasiAkses(-1)
fmt.Printf("hasil: %s\n\n", hasil)
hasil = validasiAkses(5432)
fmt.Printf("hasil: %s\n", hasil)
}
user ID tidak valid, batalkan
validasi selesai, koneksi dilepas
hasil: ditolak
akses diberikan
validasi selesai, koneksi dilepas
hasil: diizinkan
Di kedua pemanggilan, “validasi selesai, koneksi dilepas” selalu muncul — terlepas dari jalur mana yang diambil fungsi. Inilah jaminan yang diberikan defer.
Banyak Defer: Urutan LIFO
Jika satu fungsi punya beberapa defer, mereka dieksekusi dalam urutan terbalik — Last In, First Out (LIFO), seperti stack. Defer yang dideklarasikan terakhir berjalan pertama.
// audit.go
package main
import "fmt"
func prosesTransaksi(id string) {
fmt.Printf("memulai transaksi %s\n", id)
defer fmt.Printf("[1] melepas lock transaksi %s\n", id)
defer fmt.Printf("[2] menutup koneksi database\n")
defer fmt.Printf("[3] menulis log penutup\n")
fmt.Println("memproses data...")
fmt.Println("menyimpan hasil...")
}
func main() {
prosesTransaksi("TXN-20240315-001")
}
memulai transaksi TXN-20240315-001
memproses data...
menyimpan hasil...
[3] menulis log penutup
[2] menutup koneksi database
[1] melepas lock transaksi TXN-20240315-001
Urutan LIFO ini bukan kebetulan — ini dirancang agar cleanup berjalan dalam urutan terbalik dari setup. Jika kamu membuka database lalu membuka transaction, wajarnya kamu menutup transaction dulu baru menutup koneksi database. defer menjamin urutan itu secara otomatis.
Pola idiomatik Go untuk resource cleanup adalah: buka resource, lalu langsung tulis defer close(resource) di baris berikutnya. Jangan pisahkan keduanya — dengan meletakkan defer tepat setelah open, kamu tidak mungkin lupa menutup resource tersebut.
Argumen Defer Dievaluasi Saat Dideklarasikan
Ada perilaku yang sering mengejutkan: argumen yang diteruskan ke fungsi yang di-defer dievaluasi saat defer dideklarasikan, bukan saat defer dieksekusi.
// audit.go
package main
import "fmt"
func catatWaktuProses(label string) {
iterasi := 0
defer fmt.Printf("selesai di iterasi ke-%d\n", iterasi) // iterasi=0 sudah di-capture
for i := 0; i < 5; i++ {
iterasi++
fmt.Printf("memproses item %d\n", iterasi)
}
}
func main() {
catatWaktuProses("batch-harian")
}
memproses item 1
memproses item 2
memproses item 3
memproses item 4
memproses item 5
selesai di iterasi ke-0
Nilai iterasi yang tercetak adalah 0, bukan 5 — karena Go mengevaluasi iterasi pada baris defer, bukan saat defer benar-benar dieksekusi. Jika ingin menangkap nilai terkini, gunakan closure:
defer func() {
fmt.Printf("selesai di iterasi ke-%d\n", iterasi)
}()
Dengan closure, iterasi dibaca saat defer dieksekusi, sehingga nilainya adalah 5.
os.Exit: Menghentikan Program Secara Paksa
os.Exit dari package os menghentikan program secara langsung dengan kode status yang diberikan. Berbeda dari return yang hanya keluar dari satu fungsi, os.Exit mengakhiri seluruh proses Go seketika.
// audit.go
package main
import (
"fmt"
"os"
)
func periksaKonfigurasi(path string) {
if path == "" {
fmt.Println("error: path konfigurasi tidak boleh kosong")
os.Exit(1) // keluar dengan kode error
}
fmt.Printf("konfigurasi dimuat dari: %s\n", path)
}
func main() {
periksaKonfigurasi("") // akan memicu os.Exit(1)
fmt.Println("program berjalan normal") // tidak pernah tercapai
}
error: path konfigurasi tidak boleh kosong
Baris “program berjalan normal” tidak pernah muncul. os.Exit(1) menghentikan seluruh proses sebelum main sempat melanjutkan eksekusi.
Kode status yang umum digunakan:
0— sukses, program selesai normal1— error umum2— penyalahgunaan perintah shell
Interaksi Defer dan os.Exit
Inilah perbedaan paling kritis yang perlu kamu pahami: defer tidak berjalan jika program dihentikan dengan os.Exit.
// audit.go
package main
import (
"fmt"
"os"
)
func jalankanDenganExit() {
defer fmt.Println("cleanup: defer ini TIDAK akan berjalan")
fmt.Println("sebelum exit")
os.Exit(0)
fmt.Println("setelah exit") // tidak tercapai
}
func main() {
jalankanDenganExit()
}
sebelum exit
Pesan “cleanup: defer ini TIDAK akan berjalan” tidak muncul sama sekali. os.Exit memintas mekanisme defer sepenuhnya — Go runtime langsung menghentikan proses tanpa menjalankan antrian defer yang menunggu.
Jangan mengandalkan defer untuk cleanup resource kritis jika ada kemungkinan program keluar via os.Exit. File yang terbuka, koneksi database, atau temporary file tidak akan dibersihkan. Untuk kasus seperti ini, jalankan cleanup secara eksplisit sebelum memanggil os.Exit.
| Cara keluar | defer berjalan? | Keterangan |
|---|---|---|
| Akhir fungsi natural | Ya | Semua defer dieksekusi LIFO |
return | Ya | Defer berjalan sebelum return |
panic | Ya | Defer berjalan sebelum panic menyebar |
os.Exit | Tidak | Proses langsung dihentikan |
Latihan
Latihan 1 — Cleanup berjenjang:
Buat fungsi prosesLaporan(nama string) yang mensimulasikan membuka file, membuka koneksi database, dan memulai transaksi — masing-masing dengan defer untuk menutupnya. Verifikasi bahwa urutan penutupan adalah kebalikan dari urutan pembukaan.
Latihan 2 — Defer dengan closure:
Tulis fungsi yang mengukur waktu eksekusinya sendiri menggunakan defer dan closure. Fungsi mengambil waktu mulai di awal, lalu defer closure yang menghitung selisih waktu saat fungsi selesai. Gunakan time.Now() dan time.Since().
Latihan 3 — Exit dengan cleanup eksplisit:
Modifikasi contoh periksaKonfigurasi agar sebelum memanggil os.Exit, ia memanggil fungsi bersihkanResource() secara eksplisit. Tambahkan juga defer di main dan verifikasi bahwa defer di main tidak berjalan ketika os.Exit dipanggil dari dalam periksaKonfigurasi.
defer dan os.Exit melengkapi toolkit kontrol alur yang sudah kamu bangun — dari select untuk menunggu channel, timeout untuk membatasi penantian, hingga defer untuk menjamin cleanup dan os.Exit untuk penghentian paksa. Perjalanan melalui bahasa Go hampir selesai. Masih ada satu mekanisme penanganan kondisi luar biasa yang menjadi pilar terakhir: panic dan recover — bagaimana Go menangani kesalahan yang tidak terduga dan cara memulihkan program dari kondisi fatal.