BAB 32: Error, Panic, dan Recover
Kuasai tiga mekanisme penanganan kondisi luar biasa di Go: error untuk kegagalan yang bisa diprediksi, panic untuk situasi fatal, dan recover untuk pemulihan terkontrol.
Di bab sebelumnya, defer memberikan jaminan bahwa kode cleanup selalu berjalan sebelum fungsi keluar. Tapi ada situasi yang lebih ekstrem dari sekadar “fungsi keluar lebih awal” — situasi di mana sesuatu yang tidak seharusnya terjadi, terjadi. Index di luar batas array. Pembagian dengan nol. Nil pointer yang didereferensi. Go punya tiga mekanisme berbeda untuk menangani spektrum kondisi luar biasa ini: error, panic, dan recover.
Error: Kegagalan yang Bisa Diprediksi
error di Go bukan exception — ia adalah tipe data biasa. Fungsi yang bisa gagal mengembalikan error sebagai nilai balik terakhir, dan pemanggil bertanggung jawab untuk memeriksanya. Ini membuat error handling terlihat eksplisit di kode, bukan tersembunyi di balik mekanisme exception.
Tipe error adalah interface dengan satu method:
// interface error di Go
// error {
// Error() string
// }
Nilai nil pada error berarti tidak ada kesalahan. Nilai apapun selain nil berarti ada kesalahan.
// laporan.go
package main
import (
"errors"
"fmt"
"strconv"
)
func parsePeriode(input string) (int, error) {
tahun, err := strconv.Atoi(input)
if err != nil {
return 0, errors.New("periode tidak valid: input harus berupa angka tahun")
}
if tahun < 2000 || tahun > 2100 {
return 0, fmt.Errorf("periode %d di luar rentang yang didukung (2000-2100)", tahun)
}
return tahun, nil
}
func main() {
masukan := []string{"2024", "xyz", "1999", "2025"}
for _, m := range masukan {
tahun, err := parsePeriode(m)
if err != nil {
fmt.Printf("gagal memproses '%s': %v\n", m, err)
continue
}
fmt.Printf("periode valid: %d\n", tahun)
}
}
periode valid: 2024
gagal memproses 'xyz': periode tidak valid: input harus berupa angka tahun
gagal memproses '1999': periode 1999 di luar rentang yang didukung (2000-2100)
periode valid: 2025
Ada dua cara membuat error: errors.New() untuk pesan statis sederhana, dan fmt.Errorf() untuk pesan yang menyertakan nilai variabel. Keduanya menghasilkan nilai yang memenuhi interface error.
Konvensi Go: jika sebuah fungsi mengembalikan error, selalu periksa nilainya sebelum menggunakan nilai balik lain. Jangan abaikan error dengan _ kecuali kamu benar-benar yakin error tersebut tidak relevan dalam konteks itu.
Panic: Kondisi yang Tidak Bisa Dilanjutkan
panic adalah mekanisme untuk menghentikan eksekusi goroutine saat terjadi sesuatu yang tidak seharusnya terjadi sama sekali — bukan kegagalan yang bisa diprediksi, melainkan bug atau kondisi yang mengindikasikan program dalam state yang korup. Saat panic dipanggil, Go menghentikan fungsi saat ini, menjalankan semua defer yang sudah terdaftar, lalu menghentikan goroutine dengan menampilkan stack trace.
// laporan.go
package main
import "fmt"
func hitungRataRata(nilai []float64) float64 {
if len(nilai) == 0 {
// ini bukan kegagalan biasa — pemanggil seharusnya tidak memanggil
// fungsi ini dengan slice kosong. Ini adalah bug.
panic("hitungRataRata dipanggil dengan slice kosong")
}
total := 0.0
for _, n := range nilai {
total += n
}
return total / float64(len(nilai))
}
func main() {
fmt.Println(hitungRataRata([]float64{85, 90, 78, 92}))
fmt.Println(hitungRataRata([]float64{})) // akan memicu panic
}
86.25
goroutine 1 [running]:
main.hitungRataRata(...)
/laporan.go:9 +0x58
main.main()
/laporan.go:20 +0x88
exit status 2
Stack trace menunjukkan tepat di mana panic terjadi dan jalur pemanggilan yang membawa ke sana. Ini adalah informasi debug yang sangat berharga — jauh lebih informatif daripada sekadar “program berhenti.”
Go runtime sendiri memicu panic untuk sejumlah kondisi bawaan: index di luar batas array (index out of range), dereferensi nil pointer (nil pointer dereference), dan konversi interface yang gagal (interface conversion).
Recover: Memulihkan dari Panic
recover adalah fungsi yang menangkap panic yang sedang terjadi dan mengembalikan nilai yang diteruskan ke panic. Ia hanya bekerja jika dipanggil dari dalam fungsi yang di-defer — pemanggilan recover di luar konteks defer selalu mengembalikan nil dan tidak berpengaruh.
// laporan.go
package main
import "fmt"
func hitungRataRata(nilai []float64) float64 {
if len(nilai) == 0 {
panic("hitungRataRata dipanggil dengan slice kosong")
}
total := 0.0
for _, n := range nilai {
total += n
}
return total / float64(len(nilai))
}
func prosesLaporan(nama string, nilai []float64) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("peringatan: laporan '%s' dilewati karena error: %v\n", nama, r)
}
}()
rata := hitungRataRata(nilai)
fmt.Printf("laporan '%s': rata-rata %.2f\n", nama, rata)
}
func main() {
prosesLaporan("Q1-2024", []float64{88, 92, 76, 95})
prosesLaporan("Q2-2024", []float64{}) // akan panic, tapi dipulihkan
prosesLaporan("Q3-2024", []float64{70, 85, 91}) // program tetap berlanjut
}
laporan 'Q1-2024': rata-rata 87.75
peringatan: laporan 'Q2-2024' dilewati karena error: hitungRataRata dipanggil dengan slice kosong
laporan 'Q3-2024': rata-rata 82.00
Program tidak berhenti meskipun Q2 memicu panic. recover di dalam defer menangkap panic, mencetak peringatan, dan eksekusi melanjutkan ke pemanggil prosesLaporan — yaitu main — yang kemudian memanggil laporan Q3 dengan normal.
Recover di Dalam Loop
Satu pola yang berguna adalah memulihkan panic dari iterasi loop individual tanpa menghentikan loop secara keseluruhan. Caranya dengan membungkus isi loop dalam fungsi yang langsung dipanggil — teknik yang disebut IIFE (Immediately Invoked Function Expression).
// laporan.go
package main
import "fmt"
func validasiData(kode string) {
if kode == "" {
panic("kode laporan tidak boleh kosong")
}
if len(kode) > 10 {
panic(fmt.Sprintf("kode '%s' terlalu panjang (maks 10 karakter)", kode))
}
fmt.Printf("kode valid: %s\n", kode)
}
func main() {
kodeList := []string{"RPT-001", "", "RPT-002", "KODE-YANG-SANGAT-PANJANG-SEKALI", "RPT-003"}
for _, kode := range kodeList {
func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf(" dilewati — %v\n", r)
}
}()
validasiData(kode)
}() // dipanggil langsung setelah didefinisikan
}
fmt.Println("semua kode telah diproses")
}
kode valid: RPT-001
dilewati — kode laporan tidak boleh kosong
kode valid: RPT-002
dilewati — kode 'KODE-YANG-SANGAT-PANJANG-SEKALI' terlalu panjang (maks 10 karakter)
kode valid: RPT-003
semua kode telah diproses
Setiap iterasi punya scope defer-nya sendiri karena dibungkus fungsi anonim. Panic pada satu iterasi tidak menghentikan iterasi berikutnya.
Kapan Error, Kapan Panic
Perbedaan antara error dan panic sering membingungkan developer yang baru beralih ke Go. Aturan praktisnya:
| Situasi | Gunakan |
|---|---|
| Input tidak valid dari pengguna atau API eksternal | error |
| File tidak ditemukan, koneksi gagal | error |
| Fungsi dipanggil dengan argumen yang melanggar kontrak desain | panic |
| State internal program korup dan tidak bisa dilanjutkan | panic |
| Library mendeteksi kondisi yang “mustahil” terjadi | panic |
error adalah mekanisme untuk kegagalan yang bisa terjadi dalam penggunaan normal dan pemanggil perlu tahu cara menanganinya. panic adalah sinyal bahwa ada bug — sesuatu yang tidak seharusnya bisa terjadi, terjadi.
recover bukan pengganti penanganan error yang baik. Menggunakan recover untuk “menelan” semua panic secara membabi buta berbahaya — kamu bisa menyembunyikan bug yang seharusnya diperbaiki. Gunakan recover hanya saat kamu benar-benar tahu panic apa yang akan terjadi dan punya rencana pemulihan yang bermakna.
Latihan
Latihan 1 — Error bertingkat:
Buat fungsi bacaKonfigurasi(path string) (map[string]string, error) yang mengembalikan error jika path kosong, dan error berbeda jika path mengandung karakter .. (untuk mencegah path traversal). Di main, panggil dengan beberapa input berbeda dan tangani setiap error dengan pesan yang sesuai.
Latihan 2 — Panic dengan recover di middleware:
Buat fungsi jalankanSafely(nama string, fn func()) yang menjalankan fn di dalam goroutine dengan recover — jika fn panic, fungsi ini mencetak nama dan pesan error tapi tidak menghentikan program. Uji dengan beberapa fungsi: satu yang berjalan normal, satu yang panic dengan string, dan satu yang mengakses index di luar batas slice.
Latihan 3 — Kombinasi defer, panic, dan recover:
Modifikasi prosesLaporan dari contoh di atas agar ia juga mencetak “laporan selesai diproses” di akhir menggunakan defer — terlepas dari apakah terjadi panic atau tidak. Pastikan urutan output menunjukkan bahwa recover berjalan sebelum pesan “selesai”, dan pesan “selesai” tetap muncul bahkan ketika panic terjadi.
Dengan error, panic, dan recover yang sudah dikuasai, kamu sekarang punya pandangan yang lengkap tentang bagaimana Go mengelola kondisi luar biasa — dari yang paling ringan hingga yang paling fatal. Ada satu hal yang mungkin kamu sadari sepanjang bab-bab terakhir ini: semua output yang kita cetak — pesan error, laporan, log — semuanya berupa string yang diformat secara sederhana. Go punya mekanisme yang jauh lebih kaya untuk mengontrol bagaimana data ditampilkan sebagai teks, dan itulah yang akan kita bahas terakhir.