0

Giải thích cách sử dụng defer kết hợp với panic và recover để xử lý lỗi trong Golang.

Trong Golang, bộ ba defer, panicrecover chính là cơ chế sống còn để xử lý các lỗi cực kỳ nghiêm trọng (runtime errors) – những lỗi mà nếu để mặc sẽ làm toàn bộ ứng dụng bị crash.

Dưới đây là cách chúng phối hợp với nhau trong một hệ thống backend thực tế.


1. Vai trò của từng "nhân vật"

  • panic: Khi chương trình gặp một sự cố không thể tự phục hồi (ví dụ: truy cập mảng quá giới hạn, dereference một pointer nil, hoặc bạn chủ động gọi panic()), luồng thực thi bình thường của hàm sẽ bị dừng lại ngay lập tức.
  • defer: Điểm đặc biệt của Go là dù hàm có bị panic cắt ngang, ngôn ngữ này vẫn đảm bảo tất cả các lệnh defer đã được đăng ký trước đó sẽ được chạy (theo thứ tự LIFO).
  • recover: Đây là hàm duy nhất có thể "chặn đứng" sự lây lan của panic. Nó chụp lại giá trị đã được truyền vào panic và giúp Goroutine đó không bị sập. Điều kiện bắt buộc: recover chỉ có tác dụng khi nó được gọi bên trong một hàm defer.

2. Cách chúng phối hợp trong thực tế

Giả sử bạn đang viết một worker xử lý dữ liệu chạy ngầm. Nếu một job bị lỗi nghiêm trọng, bạn chỉ muốn log lỗi đó lại và bỏ qua, chứ không muốn toàn bộ tiến trình (process) chứa worker đó bị chết theo.

package main

import (
    "fmt"
)

// Giả lập một worker xử lý logic backend
func processTransaction() {
    // 1. Đăng ký defer để bọc lấy recover. Luôn đặt lên đầu hàm.
    defer func() {
        if r := recover(); r != nil {
            // Nếu có panic xảy ra, r sẽ chứa giá trị của panic đó (khác nil)
            fmt.Printf("[Cảnh báo] Đã bắt được panic: %v\n", r)
            // Ở đây bạn có thể bắn log lỗi lên Kibana, Sentry, hoặc dọn dẹp tài nguyên
        }
    }()

    fmt.Println("Worker bắt đầu xử lý giao dịch...")

    // 2. Gặp lỗi nghiêm trọng giữa chừng (ví dụ: mất kết nối DB)
    panic("Mất kết nối tới PostgreSQL đột ngột!")

    // 3. Đoạn code phía sau panic sẽ không bao giờ được chạy
    fmt.Println("Lưu giao dịch thành công (Dòng này sẽ bị bỏ qua)")
}

func main() {
    fmt.Println("--- Khởi động hệ thống ---")
    
    // Gọi hàm có khả năng sinh lỗi
    processTransaction()

    // 4. Nhờ có recover(), chương trình chính vẫn sống sót và chạy tiếp
    fmt.Println("--- Hệ thống vẫn hoạt động bình thường ---")
}

Kết quả khi chạy đoạn code trên:

--- Khởi động hệ thống ---
Worker bắt đầu xử lý giao dịch...
[Cảnh báo] Đã bắt được panic: Mất kết nối tới PostgreSQL đột ngột!
--- Hệ thống vẫn hoạt động bình thường ---

3. Lưu ý quan trọng khi thiết kế hệ thống

  • Go HTTP Server (net/http): Bạn không cần tự viết recover cho mọi API endpoint. Go router mặc định đã tích hợp sẵn recover cho từng request. Nếu một API bị panic, chỉ request đó nhận về mã lỗi 500, server vẫn tiếp tục phục vụ các request khác bình thường.
  • Tránh lạm dụng: Khác với try...catch trong các ngôn ngữ khác (Java, Node.js), bạn không nên dùng panic/recover để kiểm soát luồng logic kinh doanh (ví dụ: validate input sai, mật khẩu không đúng). Hãy sử dụng cú pháp trả về error (if err != nil) truyền thống. panic chỉ nên dùng cho những lỗi thực sự nằm ngoài tầm kiểm soát của code.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí