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:
- Lỗi tạm thời:
errors.New("...")hoặcfmt.Errorf("...")tạo ngay tại chỗ. Bên gọi chỉ đọc được thông điệp. - Sentinel error: một biến ở cấp package như
io.EOF. Bên gọi có thể kiểm tra nó bằngerrors.Is. - 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ằngerrors.Asvà đọ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ần | Cung cấp |
|---|---|
| chỉ ghi log hoặc hiển thị lỗi | fmt.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ỗi | một kiểu lỗi có các trường |
| thấy nhiều lỗi độc lập | errors.Join |
Lỗi thường gặp
- So sánh lỗi đã bọc bằng
==. Hãy dùngerrors.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.