panic, recover & defer
Panic dimaksudkan untuk kesalahan program yang tidak bisa dilanjutkan, bukan untuk kegagalan yang wajar. Tapi di server, satu panic di satu permintaan tidak boleh mematikan seluruh proses.
Intisari
panicuntuk bug,erroruntuk kegagalan yang diperkirakan. Input pengguna yang salah bukan alasan panic.recoverhanya bekerja di dalamdefer, di fungsi yang sedang panik.- Panic di goroutine mana pun mematikan seluruh proses โ
recovertidak melintasi goroutine. net/httpsudah memulihkan panic per permintaan, tapi tulis middleware sendiri supaya bisa dicatat dan dihitung.- Panic yang wajar di produksi: nil map, indeks di luar batas, type assertion tanpa
ok, pembagian nol integer.
Tiga kata kunci, satu mekanisme
func demo() {
defer fmt.Println("1. defer selalu jalan")
defer func() {
if r := recover(); r != nil {
fmt.Println("2. dipulihkan:", r)
}
}()
panic("sesuatu rusak")
fmt.Println("tidak pernah tercetak")
}
Saat panic dipanggil, fungsi berhenti, semua defer-nya dijalankan, lalu prosesnya
naik ke pemanggil dan mengulangi hal yang sama. Kalau tidak ada yang memanggil recover di
sepanjang jalan, program mati dengan mencetak stack trace.
Kapan panic sah
| Situasi | Pakai |
|---|---|
| Input pengguna tidak valid | error |
| Baris database tidak ada | error |
| API luar mengembalikan 500 | error |
| Konfigurasi wajib kosong saat startup | panic / log.Fatal โ lebih baik mati sekarang daripada salah melayani |
| Template atau regex konstan gagal dikompilasi | panic (MustCompile) โ itu bug programmer |
| Kondisi yang secara logika mustahil | panic โ dengan pesan yang menjelaskan invarian mana yang dilanggar |
// Konvensi "Must": panic saat inisialisasi, karena kalau ini gagal,
// program memang tidak layak berjalan sama sekali.
var polaEmail = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
var tmpl = template.Must(template.ParseFS(templateFS, "web/*.html"))
Batas yang jelas: panic sah sebelum server mulai melayani permintaan. Setelah itu,
kegagalan apa pun yang bisa dipicu dari luar harus jadi error โ kalau tidak, siapa pun bisa
mematikan aplikasimu dengan mengirim permintaan yang tepat.
Middleware pemulih untuk server HTTP
func Pulihkan(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec == nil {
return
}
// Klien memutus koneksi di tengah tulis: bukan bug kita.
if rec == http.ErrAbortHandler {
panic(rec)
}
slog.ErrorContext(r.Context(), "panic pada handler",
"panic", rec,
"path", r.URL.Path,
"stack", string(debug.Stack()))
w.Header().Set("Connection", "close")
http.Error(w, "terjadi kesalahan", http.StatusInternalServerError)
}()
next.ServeHTTP(w, r)
})
}
net/http memang sudah memulihkan panic sendiri supaya satu permintaan bermasalah
tidak mematikan server. Tapi pemulihan bawaan itu hanya mencetak ke log standar dan menutup koneksi โ
tanpa request_id, tanpa metrik, tanpa struktur. Middleware sendiri memberimu tiga hal yang
dibutuhkan saat insiden: stack trace lengkap, konteks permintaan, dan angka yang bisa dijadikan alarm
(Fase 9).
Panic di goroutine tidak bisa dipulihkan dari luar
// โ recover di sini TIDAK menangkap panic goroutine di bawahnya
func salah() {
defer func() { recover() }()
go func() {
panic("boom") // seluruh PROSES mati
}()
}
// โ
tiap goroutine mengurus pemulihannya sendiri
func amanJalankan(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
slog.Error("panic di goroutine", "panic", r,
"stack", string(debug.Stack()))
}
}()
f()
}()
}
Ini penyebab paling umum container mati tanpa jejak. Handler HTTP-mu terlindung middleware, tapi
goroutine latar (pemroses antrean, pembersih cache, pengirim metrik) tidak. Satu nil pointer di sana
mematikan seluruh task, dan yang terlihat di ECS hanyalah container yang restart. Setiap
go func() yang berumur panjang butuh pemulihnya sendiri.
Panic yang paling sering terjadi di produksi
| Pesan | Penyebab khas |
|---|---|
nil pointer dereference | Hasil fungsi dipakai tanpa memeriksa error lebih dulu |
index out of range | bagian[1] setelah strings.Split tanpa memeriksa panjangnya |
assignment to entry in nil map | Field map di struct tidak diisi konstruktor |
interface conversion | Type assertion tanpa bentuk , ok |
concurrent map writes | Map dipakai dari beberapa goroutine tanpa penguncian (Fase 2) |
send on closed channel | Channel ditutup oleh sisi penerima, bukan pengirim (Fase 2) |
Sejak Go 1.25, pesan panic nil pointer jauh lebih informatif โ ia menyebut ekspresi mana yang bernilai nil, bukan sekadar nomor baris. Kalau kamu masih melihat trace yang tidak menjelaskan apa pun, periksa versi Go di image produksimu.
Latihan: tulis server HTTP kecil dengan satu handler yang sengaja memanggil
panic("uji"). Panggil endpoint itu, lalu panggil endpoint lain โ buktikan servernya masih
hidup. Sekarang pindahkan panic-nya ke dalam go func(){}() di handler tersebut,
panggil sekali lagi, dan lihat seluruh prosesmu mati. Itu perbedaan yang harus kamu ingat sebelum menulis
worker di Fase 5.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.