Menu

Lỗi trong Golang: wrap, errors.Is, errors.As, kiểu lỗi riêng

Định nghĩa sentinel error và kiểu lỗi riêng, bọc lỗi bằng %w, kiểm tra chúng bằng errors.Is và errors.As, gộp nhiều lỗi bằng errors.Join, và viết phương thức Unwrap và Is khi cần.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Ba loại lỗi

Code Go dùng ba dạng lỗi (error), từ đơn giản nhất đến phong phú nhất:

  1. Lỗi tạm thời: errors.New("...") hoặc fmt.Errorf("...") tạo ngay tại chỗ. Bên gọi chỉ đọc được thông điệp.
  2. Sentinel error: một biến ở cấp package như io.EOF. Bên gọi có thể kiểm tra nó bằng errors.Is.
  3. Kiểu lỗi: một struct có phương thức Error(), mang theo các trường. Bên gọi có thể lấy nó ra bằng errors.As và đọc các trường.

Hãy chọn dạng đơn giản nhất mà vẫn cho bên gọi làm được điều họ cần.

Sentinel error

Sentinel là một giá trị lỗi được khai báo một lần và so sánh theo danh tính. Theo quy ước, tên bắt đầu bằng Err.

Đầu ra:

mug bought
cap is out of stock, notify me later
buy "hat": not found

Hai lời gọi errors.New với cùng nội dung tạo ra hai lỗi khác nhau: errors.New("x") == errors.New("x") là false. Đó là lý do sentinel phải là một biến duy nhất được dùng chung.

Sentinel trở thành một phần API của package. Khi bên gọi đã kiểm tra ErrNotFound, bạn không thể ngừng trả về nó mà không làm hỏng code của họ. Chỉ export những sentinel mà bên gọi thật sự cần để rẽ nhánh.

Kiểu lỗi tùy chỉnh

Khi bên gọi cần chi tiết (trường nào, mã trạng thái nào, thời gian chờ thử lại bao lâu), hãy định nghĩa một kiểu.

Đầu ra:

register: age: must be between 0 and 150
bad field: age

Phương thức có receiver con trỏ và hàm trả về &ValidationError{...}, nên đích của errors.As là một *ValidationError, và bạn truyền địa chỉ của nó (&ve, một **ValidationError). Nhầm mức con trỏ này là lỗi kinh điển với errors.As: với receiver giá trị, bạn sẽ trả về ValidationError{...} và khai báo var ve ValidationError. go vet bắt được sơ suất phổ biến nhất, truyền ve thay vì &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.

Bọc lỗi bằng %w

fmt.Errorf với %w trả về một lỗi nhớ lỗi mà nó bọc. Mỗi tầng thêm ngữ cảnh và giữ nguyên chuỗi lỗi.

Đầu ra:

start: connect db: timeout
  *fmt.wrapError: start: connect db: timeout
  *fmt.wrapError: connect db: timeout
  *errors.errorString: timeout
true
false

Với %v, thông điệp giống hệt nhưng chuỗi bị cắt đứt, nên errors.Is trả về false. Chọn %w khi bên gọi cần thấy được nguyên nhân, %v khi nguyên nhân là chi tiết cài đặt mà bạn không muốn họ phụ thuộc vào.

Từ Go 1.20, một lời gọi có thể bọc nhiều lỗi: fmt.Errorf("%w; %w", err1, err2). Khi đó errors.Is khớp với bất kỳ lỗi nào trong hai.

errors.Join: nhiều lỗi cùng lúc

Kiểm tra dữ liệu và dọn dẹp thường sinh ra nhiều hơn một lỗi. errors.Join (Go 1.20) gom chúng lại.

Đầu ra:

name: required
email: required
age: negative
---
true
true

errors.Join trả về nil khi mọi đối số đều nil, nên hàm ở trên không cần xử lý riêng trường hợp "không có lỗi".

Phương thức Unwrap và Is

errors.Is và errors.As tìm lỗi được bọc bằng cách gọi phương thức Unwrap. Một kiểu tùy chỉnh chứa nguyên nhân nên cung cấp nó ra ngoài:

Một kiểu bọc nhiều lỗi thì cài đặt Unwrap() []error.

Một kiểu cũng có thể định nghĩa Is(target error) bool để tự quyết định thế nào là bằng nhau, ví dụ để khớp mọi *HTTPError có cùng mã trạng thái. Bạn hiếm khi cần đến nó; bọc một sentinel và trả nó về từ Unwrap thường là đủ.

Bẫy nil có kiểu

Đừng bao giờ khai báo biến lỗi bằng kiểu cụ thể của bạn rồi trả nó về qua error:

func check() error {
	var err *ValidationError // nil pointer
	// ... no problem found
	return err // non-nil error! Its type is *ValidationError
}

if err != nil của bên gọi cho kết quả true, vì một interface chứa con trỏ nil có kiểu thì không phải nil. Hãy trả về nil trực tiếp khi thành công, và giữ các biến lỗi cục bộ ở kiểu error. Trang interfaces giải thích lý do.

Nên chọn loại nào

Bên gọi cầnCung cấp
chỉ ghi log hoặc hiển thị lỗifmt.Errorf("...: %w", err)
rẽ nhánh theo một tình huống cụ thểmột sentinel var ErrX = errors.New(...)
đọc chi tiết về lỗimột kiểu lỗi có các trường
thấy nhiều lỗi độc lậperrors.Join

Lỗi thường gặp

  • So sánh lỗi đã bọc bằng ==. Hãy dùng errors.Is.
  • Truyền giá trị không phải con trỏ cho errors.As. Nó cần một con trỏ tới một biến thuộc kiểu đích.
  • Tạo "sentinel" bên trong hàm. return errors.New("not found") tạo một giá trị mới ở mỗi lần gọi; bên gọi không thể so sánh với nó.
  • Export mọi lỗi. Mỗi sentinel hoặc kiểu được export là một cam kết API.
  • So khớp dựa trên err.Error(). Chuỗi thông điệp là để con người đọc.

Câu hỏi thường gặp

Tạo lỗi tùy chỉnh trong Go như thế nào?

Với một tình huống cố định, hãy khai báo một sentinel ở cấp package: var ErrNotFound = errors.New("not found"). Với một lỗi mang dữ liệu, hãy định nghĩa một kiểu có phương thức Error() string: type ValidationError struct { Field string } và func (e *ValidationError) Error() string { return e.Field + " is invalid" }.

Bọc (wrap) lỗi trong Go như thế nào?

Dùng fmt.Errorf với verb %w: return fmt.Errorf("load user %d: %w", id, err). Thông điệp của lỗi mới chứa thông điệp của lỗi cũ, và errors.Unwrap, errors.Is, errors.As vẫn chạm tới được lỗi gốc. Từ Go 1.20, một lời gọi Errorf có thể bọc nhiều lỗi bằng nhiều verb %w.

errors.Is và errors.As khác nhau thế nào?

errors.Is(err, target) trả lời câu hỏi "giá trị lỗi cụ thể này có nằm đâu đó trong chuỗi không", dùng cho các sentinel như io.EOF. errors.As(err, &target) trả lời câu hỏi "trong chuỗi có lỗi nào thuộc kiểu này không", và nếu có thì lưu nó vào target để bạn đọc các trường của nó.

errors.Join làm gì?

errors.Join(errs...) (Go 1.20) gộp nhiều lỗi thành một. Thông điệp của nó là các thông điệp riêng lẻ ngăn cách bằng dấu xuống dòng, các lỗi nil bị bỏ qua, và nó trả về nil nếu tất cả đều nil. errors.Is và errors.As khớp nếu bất kỳ lỗi nào trong số được gộp khớp.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu