Error handling in Golang
A quote from Go Proverbs:
Don’t just check errors, handle them gracefully.
Hạn chế Sentinel errors
Sentinel errors là cách xử lý error một cách quá cụ thể. Như việc tạo custom error type cho một lỗi nhất định rồi “đóng gói” thêm một vài thông tin về error đó.
Ví dụ với error types
:
type DivisionByZeroError struct {
Message string
Line int
}
func (e *DivisionByZeroError) Error() string {
return e.Message
}
result, err := Divide(10, 0) // if divide by zero, DivisionByZeroError will be returned
switch err := err.(type) {
case nil:
// call succeeded, nothing to do
case *DivisionByZeroError:
fmt.Println("error occurred on line:", err.Line)
default:
// unknown error
}
Một ví dụ khác đơn giản hơn dùng error value
:
var EOF = errors.New("EOF")
Sau hai ví dụ ta có thể thấy error types
đôi phần ổn hơn vì có khả năng wrap
thêm thông tin vào error.
Question: Có thể xử lý error theo từng trường hợp cụ thể và cả khả năng wrap error nữa, vậy quá tốt chứ sao lại hạn chế?
Có ba vấn đề chính:
- Nếu
wrap error
không cẩn thận có thể che giấu error thật sự. - Vì nó quá cụ thể nên các functions và packages sẽ bị phụ thuộc lên nhau, đồng thời cũng giảm tính abstract -> Tăng độ phức tạp.
error types
bắt buộc phải exported để cho caller gọi kiểm tra (type assertion or type switch)
Tránh inspecting error
Không bao giờ kiểm tra error.Error() trong code. Ví dụ:
file, err := os.Open("example.txt")
if err.Error() == "file does not exist" {
// do something
}
Vì sao?
- Function này để trả về error message cho người dùng cuối đọc, không phải chương trình của bạn!
- Lỡ một ngày library cập nhật mới thay đổi message đó thì chương trình của bạn tèo.
Cách mình thường dùng
Ý tưởng là có lỗi thì trả về lỗi, đơn giản vậy thôi:
file, err := os.Open("example.txt")
if err != nil {
return err
}
Để hoàn thiện ý tưởng mình sẽ phân loại error thành:
- Lỗi thuộc về người dùng để trả về message tương ứng.
- Lỗi unexpected thuộc về lập trình viên, lúc này ta cần
wrap
lỗi với nhiều thông tin để tiện cho việc debug. - Lỗi tạm thời, hỗ trợ function trong việc retry chẳng hạn.
Okay, việc bây giờ là viết wrapper function cho từng trường hợp thôi ^^
// case 1
func WithInvalid(err error) error {
if err == nil {
return nil
}
return &withInvalid{
err,
callers(),
}
}
// case 2
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
// case 3
func WithTemporary(err error) error {
if err == nil {
return nil
}
return &withTemporary{
err,
callers(),
}
}
Cùng hoàn thiện ý tưởng nào:
file, err := os.Open("example.txt")
if err != nil {
return errors.WithStack(err)
}
Đọc thêm Github repo này để hiểu rõ về cách wrap
và stack trace
error nhé!
Bạn cũng có thể tham khảo thêm blog này: Error handling