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.
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
Dividenhậ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
SafeDividenhậ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
defercù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
Dividebên trong - Trả về kết quả và
nilnếu thành công, hoặc 0 và lỗi nếu khôi phục được sau panic
- Dùng
main.go: Đọc hai số nguyên từ đầu vào và dùngSafeDivideđể thực hiện phép tính một cách an toàn. Đồng thời, minh họa cách dùngdeferbằ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 inCleanup complete - Dùng một
deferkhác để lên lịch inReleasing resources - Gọi
SafeDividevới các giá trị đầu vào - In
Result: [value]hoặcError: [error message]tùy theo kết quả
- In
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 completeVới 10 và 0, đầu ra sẽ là:
Starting calculation
Error: calculation error: division by zero
Releasing resources
Cleanup completeTự làm thử
package main
import (
"fmt"
)
func main() {
// Đọc đầu vào
var dividend, divisor int
fmt.Scanln(÷nd)
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
}
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
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online