Go Zero to Hero - Bài 43: Quản lý khủng hoảng với panic và recover
Trong hệ thống định kiểu tĩnh và xử lý lỗi minh bạch của Golang, ngoại lệ nghiêm trọng được quản lý qua hai hàm tích hợp sẵn: panic và recover.
1. panic: Nút bấm "Tự hủy" của hệ thống
Trong Go, panic là một hàm tích hợp sẵn. Khi panic được gọi (bởi bạn, hoặc do lỗi hệ thống như truy cập mảng quá giới hạn), luồng thực thi hiện tại sẽ lập tức dừng lại. Go sẽ bắt đầu chạy ngược lên trên, thực thi các lệnh defer (nếu có), và cuối cùng là đánh sập (crash) toàn bộ chương trình, in ra Stack Trace đỏ lòm.
package main
import "fmt"
func processImportantData() {
fmt.Println("1. Đang đọc dữ liệu...")
// Cố tình kích hoạt panic vì phát hiện lỗi không thể cứu vãn
panic("Mất kết nối Database lõi! Hệ thống không thể tiếp tục!")
// Dòng này SẼ KHÔNG BAO GIỜ được chạy
fmt.Println("2. Đã xử lý xong!")
}
func main() {
processImportantData()
fmt.Println("Server vẫn đang chạy bình thường...") // Không bao giờ chạy tới đây
}
Khi nào thì dùng panic?
Nguyên tắc của giới Gopher: "Don't panic". Bạn chỉ nên chủ động gọi panic() khi gặp lỗi vô phương cứu chữa ngay từ lúc khởi động server (ví dụ: Không đọc được file cấu hình .env, cấu hình Database sai thông tin). Trong quá trình server đang phục vụ user, tuyệt đối phải trả về biến error, không được dùng panic.
2. recover: Chiếc túi khí cứu sinh (Airbag)
Nếu panic là vụ va chạm xe hơi, thì recover chính là chiếc túi khí. Nó bắt lấy lỗi panic, ngăn không cho chương trình sập, và cho phép hệ thống tiếp tục hoạt động.
Tuy nhiên, có 2 quy luật thép đối với recover:
- Nó BẮT BUỘC phải được gọi bên trong một hàm ẩn danh có từ khóa
defer(vì khipanicxảy ra, chỉ có các hàmdefermới được hệ thống lôi ra chạy). - Nó chỉ cứu được luồng (
Goroutine) hiện tại.
Hãy xem cách chúng ta lắp "túi khí" cho hàm ở trên:
package main
import "fmt"
func processImportantDataSafe() {
// 1. Đặt túi khí LÊN ĐẦU HÀM bằng defer
defer func() {
// Hàm recover() sẽ trả về nội dung của panic nếu có
if err := recover(); err != nil {
fmt.Println("🚨 [TÚI KHÍ HOẠT ĐỘNG] Bắt được lỗi:", err)
fmt.Println("🛠 Đang ghi log và dọn dẹp hệ thống... Server an toàn!")
}
}() // Đừng quên cặp ngoặc () để thực thi Closure này
fmt.Println("1. Đang xử lý giao dịch vé...")
// 2. Kích hoạt panic giả lập (hoặc một lỗi nil pointer ngoài ý muốn)
panic("BÙM! Lỗi chia cho 0 hoặc Null Pointer!")
fmt.Println("2. Dòng này vẫn không chạy...")
}
func main() {
processImportantDataSafe()
// ĐIỀU KỲ DIỆU LÀ ĐÂY:
fmt.Println("✅ Server vẫn sống khỏe và phục vụ các request tiếp theo!")
}
Khi chạy đoạn code trên, dù hàm processImportantDataSafe bị nổ tung từ bên trong, luồng chính của main vẫn sống sót và tiếp tục chạy.
3. Ứng dụng thực chiến: Middleware "Hồi sinh" trong Web Framework
Nếu bạn thắc mắc tại sao các framework như Gin hay Echo không bao giờ bị sập dù code bên trong API của bạn thi thoảng bị lỗi Null Pointer, câu trả lời là do họ đã bọc mọi HTTP Request vào một Middleware có chứa recover.
Nếu tự viết một HTTP Server thuần (Standard Library), bạn sẽ phải tự viết cái khiên bảo vệ đó:
package main
import (
"fmt"
"net/http"
)
// Middleware bọc lấy mọi Request để đảm bảo Server không bao giờ Crash
func RecoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Cài cắm defer và recover ngay tại cửa ngõ vào API
defer func() {
if err := recover(); err != nil {
// 1. Ghi log lỗi ra console
fmt.Println("[CRITICAL PANIC CỨU ĐƯỢC]:", err)
// 2. Trả về mã 500 cho Client thay vì ngắt kết nối đột ngột
http.Error(w, "Lỗi máy chủ nội bộ", http.StatusInternalServerError)
}
}()
// Chuyển quyền thực thi cho các hàm logic API bên trong
next.ServeHTTP(w, r)
}
}
// Hàm API bị lỗi cực nặng
func badAPIHandler(w http.ResponseWriter, r *http.Request) {
var p *int // p đang là nil
*p = 10 // 🚨 KÍCH HOẠT LỖI PANIC (Nil Pointer Dereference)
w.Write([]byte("Sẽ không bao giờ chạy tới đây"))
}
func main() {
// Bọc API bằng Middleware
safeHandler := RecoveryMiddleware(badAPIHandler)
http.HandleFunc("/api/data", safeHandler)
fmt.Println("Server chạy tại port 8080...")
http.ListenAndServe(":8080", nil)
}
Nhờ có đoạn Middleware này, khi một User gọi vào API và gây ra lỗi Null Pointer, chỉ có request của User đó bị hủy và nhận về mã 500. Hàng ngàn User khác đang truy cập vào hệ thống vẫn hoàn toàn không bị ảnh hưởng!
Tổng kết
Bạn đã nắm giữ bí quyết sinh tồn cuối cùng của một ứng dụng Go:
- Panic: Là lỗi chí mạng khiến hệ thống bị sập. Chỉ chủ động dùng khi ứng dụng vừa khởi động và thiếu tài nguyên cấu hình.
- Recover: Là phép thuật hồi sinh. Phải luôn được đặt bên trong một
defer func(). - Kỷ luật Backend: Luôn có một lớp Recovery Middleware bọc ở vòng ngoài cùng của các ứng dụng Web/Microservices để đảm bảo tính sẵn sàng (High Availability) 99.99%.
All rights reserved