Panic trông như thế nào
Panic dừng hàm hiện tại, chạy các lời gọi đã defer của nó, rồi làm tương tự ở hàm gọi nó, cứ thế đi ngược lên ngăn xếp. Nếu lên tới đỉnh của goroutine, chương trình sẽ sập.
Chương trình này thoát với mã trạng thái 2. Đầu ra là:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
Lời gọi đã defer chạy trước báo cáo sự cố. Stack trace ghi tên goroutine, hàm và dòng code, thường là đủ để tìm ra bug.
Các panic runtime thường gặp
| Thông báo | Nguyên nhân |
|---|---|
index out of range [5] with length 3 | chỉ số của slice, mảng hoặc chuỗi vượt quá cuối |
slice bounds out of range [:7] with capacity 5 | cắt slice vượt quá dung lượng |
invalid memory address or nil pointer dereference | đọc trường hoặc gọi qua một con trỏ nil |
assignment to entry in nil map | ghi vào một map chưa bao giờ được tạo |
interface conversion: interface {} is int, not string | type assertion dạng một giá trị sang sai kiểu |
integer divide by zero | chia nguyên hoặc chia lấy dư cho 0 (số thực thì cho ra +Inf hoặc NaN) |
close of closed channel, send on closed channel | dùng sai channel |
all goroutines are asleep - deadlock! | mọi goroutine đều bị chặn (fatal error, không phải panic) |
Mỗi lỗi ở trên là một bug trong chương trình, không phải một tình huống cần xử lý. Cách sửa là kiểm tra giới hạn, kiểm tra nil, gọi make, hoặc dùng assertion dạng comma-ok, chứ không phải recover.
Recover
recover() dừng một panic. Nó chỉ hoạt động khi được gọi trực tiếp bên trong một hàm được defer, vì hàm được defer là code duy nhất chạy trong lúc panic tháo ngăn xếp.
Đầu ra:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Chuyện gì đã xảy ra ở lần gọi thứ hai:
a / bgây panic.- Closure được defer chạy, và
recover()trả về giá trị panic (mộtruntime.Error). - Việc tháo ngăn xếp dừng lại.
safeDividetrả về bình thường chomain, với named resulterrđược closure gán giá trị.
Chính named result cho phép hàm được defer trả lỗi về. Nếu không có nó, hàm trả về các giá trị zero. Trang defer trình bày cách closure được defer thay đổi kết quả.
recover() trả về nil khi không có panic, nên phép kiểm tra if r != nil giúp hàm được defer vô hại ở luồng bình thường. Gọi bên ngoài hàm được defer, hoặc trong một hàm được hàm defer gọi tới, recover trả về nil và không làm gì.
panic với giá trị của riêng bạn
panic nhận bất kỳ giá trị nào. Thường là một error hoặc một chuỗi.
Hãy recover những gì bạn lường trước và panic lại với mọi thứ khác. Nuốt hết mọi panic sẽ che giấu các bug thật.
Từ Go 1.21, panic(nil) được chuyển thành *runtime.PanicNilError, nên giờ recover() trả về nil chắc chắn có nghĩa là "không có panic".
Panic trong goroutine
recover chỉ bắt được panic trong goroutine của chính nó. Một panic trong bất kỳ goroutine nào không có recover sẽ giết cả tiến trình, bao gồm main và mọi goroutine khác.
Hai dòng của worker có thể in ra theo thứ tự bất kỳ; main finished luôn ở cuối. Một defer recover() trong main sẽ không cứu được chương trình khỏi worker thứ hai. Đây là lý do máy chủ HTTP recover theo từng request: net/http recover các panic trong goroutine của mỗi handler, ghi log và đóng kết nối đó, để một request lỗi không đánh sập cả máy chủ.
Một số sự cố là fatal error, không phải panic, và hoàn toàn không recover được: concurrent map writes, hết bộ nhớ, và all goroutines are asleep của bộ phát hiện deadlock.
Khi nào panic là lựa chọn đúng
Nguyên tắc chung của Go: trả về lỗi cho mọi thứ có thể hỏng lúc chạy, và chỉ panic khi lập trình viên mắc lỗi. Cụ thể, panic phù hợp khi:
- Một bất biến bị phá vỡ. Một
switchtrên enum của chính bạn rơi vào trường hợp không thể xảy ra. Chạy tiếp sẽ làm hỏng dữ liệu. - Một hàm hỗ trợ
Mustnhận đầu vào hằng số sai.regexp.MustCompile,template.Mustvàuuid.MustParsebọc một hàm trả về lỗi và panic khi thất bại. Hãy dùng chúng cho các giá trị biết trước lúc biên dịch, thường là biến cấp package, nơi thất bại có nghĩa là mã nguồn sai:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Không thể khởi động tiếp. Thiếu cấu hình bắt buộc trong
main. Ngay cả khi đó, in lỗi ra rồi gọios.Exit(1)thường gọn gàng hơn một stack trace.
Panic là công cụ sai cho:
- Những thất bại lường trước được: đầu vào không hợp lệ của người dùng, thiếu tệp, hết thời gian chờ. Hãy trả về
error; xem xử lý lỗi. - Điều khiển luồng: dùng panic và recover như exception xuyên qua một cây lời gọi lớn khiến code khó theo dõi. Thư viện chuẩn làm vậy ở vài chỗ bên trong (bộ mã hóa của
encoding/json), nhưng luôn recover trước khi trả về, nên không panic nào lọt ra khỏi package. - API của thư viện: một thư viện panic khi gặp đầu vào sai buộc mọi nơi gọi phải thêm recover. Hãy trả về lỗi.
Lỗi thường gặp
- Gọi recover bên ngoài hàm được defer. Nó trả về
nil. - Recover trong
maincho panic của một goroutine. Mỗi goroutine cần recover của riêng nó. - Nuốt hết mọi panic. Ghi log kèm stack (
debug.Stack()từruntime/debug) và panic lại những gì bạn không lường trước. - Dùng
recoverđể xử lý việc ghi vào map nil hay chỉ số vượt phạm vi. Hãy sửa bug.
Câu hỏi thường gặp
Panic trong Go là gì?
Là một sự cố lúc chạy làm dừng luồng bình thường của goroutine hiện tại. Go chạy các lời gọi đã defer của từng hàm trên ngăn xếp, từ trong cùng ra ngoài, và nếu không có gì recover, chương trình in ra giá trị panic cùng stack trace rồi thoát với mã trạng thái 2. Panic đến từ bug (chỉ số vượt phạm vi, giải tham chiếu con trỏ nil, ghi vào map nil) hoặc từ một lời gọi panic(v) tường minh.
Làm sao recover sau một panic trong Go?
Gọi recover() bên trong một hàm được defer: defer func() { if r := recover(); r != nil { ... } }(). Nó trả về giá trị đã truyền cho panic và dừng việc tháo ngăn xếp, nên hàm đã defer nó sẽ trả về bình thường cho nơi gọi. Gọi ở bất kỳ chỗ nào khác, recover trả về nil và không làm gì.
Có recover được panic từ một goroutine khác không?
Không. recover chỉ dừng panic trong goroutine nơi nó chạy. Một panic trong goroutine bạn khởi chạy mà bên trong goroutine đó không có recover sẽ làm sập cả chương trình. Mỗi goroutine có thể panic đều cần recover được defer của riêng nó.
Khi nào nên dùng panic thay vì trả về lỗi?
Cho bug và trạng thái không thể xảy ra, không dành cho những thất bại lường trước được. Đầu vào sai, thiếu tệp và lỗi mạng là lỗi. Panic hợp lý khi một bất biến bị phá vỡ, khi một hàm hỗ trợ Must nhận một hằng số lẽ ra luôn hợp lệ (regexp.MustCompile), hoặc khi chương trình hoàn toàn không thể khởi động. Thư viện không nên để panic lọt ra ngoài API public của mình.