Menu
CoddyTech

Panic, defer và recover

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 57 trên 107.

Dù Go chú trọng việc trả về lỗi, một số tình huống thực sự không thể khắc phục. Trong những trường hợp này, Go cung cấp panic, defer và recover: một cơ chế để xử lý các tình huống ngoại lệ.

panic dừng ngay quá trình thực thi bình thường. Từ này dành riêng cho các lỗi lập trình (programming errors), chẳng hạn như truy cập chỉ số vượt phạm vi (out-of-bounds index), chứ không dùng cho các lỗi thường gặp như dữ liệu đầu vào không hợp lệ của người dùng (invalid user input):

func MustGetConfig(key string) string {
    value, exists := config[key]
    if !exists {
        panic("missing required config: " + key)
    }
    return value
}

defer lên lịch để một hàm chạy khi hàm bao quanh kết thúc, dù kết thúc bình thường hay do panic. Các lời gọi được trì hoãn sẽ chạy theo thứ tự ngược lại (vào sau, ra trước):

func ProcessFile() {
    fmt.Println("Opening file")
    defer fmt.Println("Closing file")
    fmt.Println("Processing...")
    // Đầu ra: Opening file, Processing..., Closing file
}

recover bắt một panic và đưa chương trình trở lại trạng thái thực thi bình thường. Nó chỉ hoạt động bên trong một hàm được gọi bằng defer:

func SafeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    
    riskyOperation()
    return nil
}

Mẫu này chuyển một panic thành lỗi thông thường, giúp chương trình tiếp tục chạy. Chỉ dùng cách này khi thật cần thiết: ưu tiên trả về lỗi cho các trường hợp thất bại có thể dự đoán, và chỉ dùng panic/recover cho những tình huống thực sự bất thường như trạng thái bị hỏng hoặc tình huống không thể khôi phục.

challenge icon

Thử thách

Dễ

Hãy xây dựng một máy tính chia an toàn để minh họa cách dùng panic, defer và recover để xử lý các tình huống bất thường một cách ổn thỏa. Máy tính sẽ thử thực hiện các phép toán có rủi ro và chuyển panic thành lỗi thông thường để có thể xử lý như bình thường.

Bạn sẽ sắp xếp code trong hai tệp:

  • calculator.go: Tạo logic chia có khả năng khôi phục sau panic.

    Triển khai hàm Divide nhận hai số nguyên và phát sinh panic với thông báo "division by zero" nếu số chia bằng 0. Nếu không, hàm trả về kết quả chia dưới dạng số nguyên.

    Triển khai hàm SafeDivide nhận hai số nguyên và trả về cả kết quả dạng số nguyên lẫn một lỗi. Hàm này cần:

    • Dùng defer cùng một hàm ẩn danh để khôi phục sau mọi panic
    • Nếu khôi phục được sau panic, chuyển panic thành lỗi theo định dạng "calculation error: [panic message]"
    • Gọi hàm Divide bên trong
    • Trả về kết quả và nil nếu thành công, hoặc 0 và lỗi nếu khôi phục được sau panic
  • main.go: Đọc hai số nguyên từ đầu vào và dùng SafeDivide để thực hiện phép tính một cách an toàn. Đồng thời, minh họa cách dùng defer bằng cách in các thông báo dọn dẹp theo đúng thứ tự.

    Hàm main của bạn cần:

    • In Starting calculation ở đầu tiên
    • Dùng defer để lên lịch in Cleanup complete
    • Dùng một defer khác để lên lịch in Releasing resources
    • Gọi SafeDivide với các giá trị đầu vào
    • In Result: [value] hoặc Error: [error message] tùy theo kết quả

Các đầu vào sẽ được cung cấp như sau:

  • Dòng 1: Số nguyên thứ nhất (số bị chia)
  • Dòng 2: Số nguyên thứ hai (số chia)

Hãy nhớ rằng các lời gọi được defer sẽ thực thi theo thứ tự ngược lại, tức vào sau ra trước, vì vậy các thông báo dọn dẹp sẽ xuất hiện theo thứ tự ngược với thứ tự bạn dùng defer.

Ví dụ, với 20 và 4, đầu ra sẽ là:

Starting calculation
Result: 5
Releasing resources
Cleanup complete

Với 10 và 0, đầu ra sẽ là:

Starting calculation
Error: calculation error: division by zero
Releasing resources
Cleanup complete

Tự làm thử

package main

import (
	"fmt"
)

func main() {
	// Đọc đầu vào
	var dividend, divisor int
	fmt.Scanln(&dividend)
	fmt.Scanln(&divisor)

	// In ra thông báo bắt đầu
	fmt.Println("Starting calculation")

	// TODO: Dùng defer để lên lịch in thông báo "Cleanup complete"

	// TODO: Dùng defer để lên lịch in thông báo "Releasing resources"
	// Ghi nhớ: các lệnh gọi trì hoãn chạy theo thứ tự LIFO (vào sau, ra trước)

	// TODO: Gọi SafeDivide với các giá trị đầu vào

	// TODO: In ra "Result: [value]" hoặc "Error: [error message]"
	// tùy vào việc hàm có trả về lỗi hay không
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online