Go có panic và recover, và nhìn qua thì giống throw và catch. Chúng không phải vậy, và dùng nhầm là cách nhanh nhất để viết mã Go trông như Java.
Panic dừng cả chương trình
panic("nổ")
Panic bắt đầu tháo ngăn xếp: mọi defer trên đường đi được chạy, rồi chương trình in dấu vết và thoát với mã 2.
Ba nguồn panic bạn sẽ gặp:
Lỗi lập trình lúc chạy — truy cập con trỏ nil, chỉ số ngoài mảng, chia số nguyên cho 0, ghi vào map nil (bài 10), type assertion sai không có comma-ok (bài 14).
Bạn tự gọi panic().
log.Fatal — thực ra không panic mà gọi os.Exit(1) ngay, nên defer không chạy. Chi tiết này quan trọng: log.Fatal trong hàm có defer f.Close() sẽ bỏ qua việc đóng tệp.
recover chỉ chạy trong defer
func phucHoi() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("đã phục hồi từ panic: %v", r)
}
}()
panic("nổ")
}
đã phục hồi từ panic: nổ
recover() gọi ở đâu khác đều trả về nil và không làm gì. Đây là ràng buộc cố ý — nó khiến bạn không thể rắc recover khắp nơi như try/catch.
Chú ý mẫu ở trên dùng named return để đổi giá trị trả về thành lỗi. Đây là một trong số ít trường hợp named return thật sự cần, như bài 7 đã nói.
Khi nào được phép panic
Danh sách rất ngắn:
Lỗi lập trình không thể tiếp tục — bất biến bị vi phạm, trạng thái không thể xảy ra. Ví dụ trong default của một switch mà mọi trường hợp đều đã liệt kê.
Khởi tạo hằng hỏng, lúc khởi động. Đây là quy ước Must...:
var re = regexp.MustCompile(`^[a-z]+$`)
Regex hằng sai là lỗi lập trình, và chết ngay lúc khởi động tốt hơn nhiều so với chết lúc phục vụ request.
Trong hàm khởi tạo của package khi thiếu thứ bắt buộc.
Ngoài ba trường hợp đó: trả về error. Panic trong thư viện là hành vi bị coi là thô lỗ — bạn quyết định thay cho người dùng rằng chương trình của họ phải chết.
Khi nào được phép recover
Cũng rất ngắn:
Ở ranh giới ngoài cùng của một tác vụ độc lập — một handler HTTP, một job trong hàng đợi. Một request hỏng không nên giết cả máy chủ.
net/http đã làm sẵn việc này: panic trong handler chỉ giết request đó, không giết server. Nhưng nó chỉ in dấu vết ra stderr, nên bạn thường vẫn muốn middleware riêng để log cho tử tế:
func PhucHoi(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v\n%s", rec, debug.Stack())
http.Error(w, "lỗi máy chủ", 500)
}
}()
next.ServeHTTP(w, r)
})
}
debug.Stack() là phần quan trọng — không có nó bạn chỉ biết "có panic" mà không biết ở đâu.
Chuyển panic của thư viện bên thứ ba thành lỗi. Khi bạn buộc phải dùng một thư viện hay panic.
Ngoài ra: đừng recover. Đặc biệt đừng recover rồi tiếp tục như không có gì — trạng thái chương trình lúc đó không còn tin được.
Cái bẫy: panic trong goroutine
Đây là chỗ nguy hiểm nhất, và tôi sẽ nói kỹ hơn ở chặng đồng thời:
go func() {
panic("nổ trong goroutine")
}()
Panic trong một goroutine giết cả chương trình, và recover ở goroutine cha không bắt được. Mỗi goroutine phải tự lo recover của mình.
Nghĩa là mọi goroutine chạy mã có thể panic đều cần:
go func() {
defer func() {
if r := recover(); r != nil { log.Printf("panic: %v", r) }
}()
lam()
}()
Đây là lý do các thư viện worker pool trong Go đều có sẵn cơ chế này.
Panic mang giá trị bất kỳ
panic(fmt.Errorf("lỗi: %w", err))
recover() trả về any, nên bạn kiểm kiểu như bài 14:
if r := recover(); r != nil {
if err, ok := r.(error); ok { ... }
}
Quy ước: panic bằng error hoặc chuỗi, không panic bằng kiểu tự chế — người recover sẽ không biết kiểm gì.
Thử ba mươi giây
func f() {
defer fmt.Println("defer CHẠY")
log.Fatal("chết")
}
Dòng defer không in ra. log.Fatal gọi os.Exit trực tiếp, bỏ qua toàn bộ cơ chế tháo ngăn xếp.
Ba mươi giây đó giải thích vì sao log.Fatal chỉ nên xuất hiện trong main(), không bao giờ trong thư viện.
Ngày mai: defer — thứ tự LIFO và cái bẫy đối số tính ngay.