Làm dev đau lưng

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:

  1. Nếu wrap error không cẩn thận có thể che giấu error thật sự.
  2. 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.
  3. 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:

  1. Lỗi thuộc về người dùng để trả về message tương ứng.
  2. 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.
  3. 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 wrapstack trace error nhé!

Bạn cũng có thể tham khảo thêm blog này: Error handling