Lỗi là giá trị
Một hàm Go có thể thất bại sẽ trả về một error làm kết quả cuối cùng. Bên gọi kiểm tra nó ngay lập tức.
Đầu ra:
parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax
Đó là toàn bộ cơ chế. Không có ngoại lệ (exception), không có try hay catch, và không có luồng điều khiển ẩn: một lỗi chỉ đi đến nơi mà code của bạn truyền nó tới. Cái giá phải trả là if err != nil lặp lại rất nhiều lần. Đổi lại, mọi điểm có thể thất bại đều hiện rõ trên trang, và tại mỗi điểm bạn tự quyết định điều gì sẽ xảy ra.
Kiểu error
error là một interface có sẵn với một phương thức:
type error interface {
Error() string
}
Bất kỳ kiểu nào có phương thức Error() string đều là một error. Một error bằng nil nghĩa là thành công. In một error bằng fmt.Println(err) hoặc %v sẽ gọi Error().
Tạo lỗi
Hai hàm đủ cho hầu hết trường hợp.
errors.New tạo một lỗi với nội dung cố định. fmt.Errorf định dạng lỗi với cùng các verb như Printf. Theo quy ước, chuỗi lỗi bắt đầu bằng chữ thường và không có dấu câu ở cuối, vì chúng thường được lồng vào những thông báo dài hơn: load config: open app.yaml: no such file or directory.
Mẫu if err != nil
Cấu trúc chuẩn mực là: gọi, kiểm tra, trả về sớm. Luồng thành công nằm sát lề trái, và mỗi lỗi thoát ra ngay khi xảy ra.
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, err
}
u, err := parseUser(row)
if err != nil {
return nil, err
}
if err := u.Validate(); err != nil {
return nil, err
}
return u, nil
}
Hai quy ước cần chú ý:
- Khi có lỗi, trả về giá trị zero cho các kết quả còn lại (
nil,0,""). Bên gọi không được dùng chúng khierr != nil. if err := f(); err != nilgiới hạn phạm vi củaerrtrongifkhi hàm chỉ trả về một lỗi. Cách này giữ cho phạm vi bên ngoài gọn gàng.
Tránh dùng else sau một lệnh trả về lỗi. if err != nil { return err } else { ... } chỉ làm luồng chính thụt lề vô ích.
Thêm ngữ cảnh khi trả về lỗi
Một lỗi được chuyển lên nguyên vẹn sẽ mất đi câu chuyện về nơi nó xuất phát. open config.yaml: no such file or directory không cho bạn biết bước nào trong quá trình khởi động đã thất bại. Hãy thêm ngữ cảnh bằng fmt.Errorf và verb %w:
Đầu ra:
start server: read config: open /etc/myapp/config.yaml: no such file or directory
true
Mỗi lớp thêm vào việc nó đang làm, và thông báo cuối cùng đọc như một dấu vết từ đầu chuỗi lời gọi xuống tận nguyên nhân. Ngữ cảnh tốt nêu tên thao tác và đầu vào: parse line 12, fetch user 42. Đừng thêm "error" hay "failed" ở mọi cấp; bản thân thông báo đã là một lỗi.
%w wrap lỗi: nó giữ lỗi gốc bên trong lỗi mới, nên errors.Is và errors.As vẫn tìm thấy lỗi gốc. %v chỉ sao chép phần văn bản. Dùng %v khi bạn cố ý muốn che giấu chi tiết cài đặt khỏi bên gọi, chẳng hạn để họ không phụ thuộc vào kiểu lỗi của một driver cơ sở dữ liệu.
Kiểm tra lỗi cụ thể: errors.Is và errors.As
Đôi khi bên gọi cần phản ứng với một loại lỗi nhất định: thiếu tệp nghĩa là "dùng giá trị mặc định", hết thời gian chờ nghĩa là "thử lại". Hai hàm giải quyết việc này, và cả hai đều nhìn xuyên qua mọi lớp wrap.
Một vài nguyên tắc:
- So sánh với các giá trị lỗi định nghĩa sẵn (sentinel như
io.EOF,os.ErrNotExist,sql.ErrNoRows) bằngerrors.Is, không dùng==.==sẽ sai khi lỗi đã được wrap. - Lấy ra một lỗi có kiểu bằng
errors.As, không dùng type assertion, cũng vì lý do đó.errors.Asnhận một con trỏ tới biến thuộc kiểu đích. - Đừng bao giờ so khớp theo nội dung
err.Error(). Thông báo thay đổi giữa các phiên bản, và việc so khớp văn bản sẽ âm thầm hỏng khi điều đó xảy ra.
Cách tự định nghĩa lỗi sentinel và kiểu lỗi riêng, cũng như gộp nhiều lỗi bằng errors.Join, được trình bày ở trang lỗi tùy chỉnh.
Xử lý mỗi lỗi một lần
Một lỗi nên được xử lý đúng một lần. Xử lý nghĩa là một trong các việc: trả nó về (thường là sau khi wrap), ghi log rồi tiếp tục, thử lại, hoặc biến nó thành phản hồi cho người dùng. Làm hai trong số những việc này là bug về lỗi phổ biến nhất trong code Go.
// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
log.Printf("could not fetch user: %v", err)
return err
}
// Right: add context and return. The top of the program logs once.
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
Vừa ghi log vừa trả về khiến cùng một lỗi xuất hiện nhiều lần trong log, mỗi lần lại ít ngữ cảnh hơn thông báo cuối cùng. Hãy để lỗi đi lên tới nơi có thể quyết định phải làm gì (một HTTP handler, một main, một vòng lặp worker), và ghi log tại đó.
Lỗi cuối cùng đi về đâu
Ở tầng trên cùng của chương trình, phải có thứ gì đó xử lý lỗi. Trong main, việc đó thường là in lỗi ra và thoát với mã trạng thái khác 0:
Chạy chương trình này không kèm đối số sẽ in error: usage: app <name> ra stderr và thoát với mã trạng thái 1 (nhập một tên vào ô Args để xem nhánh còn lại). Giữ main theo cấu trúc này, với phần việc thật nằm trong run, giúp chương trình dễ kiểm thử và đảm bảo các câu lệnh defer bên trong run vẫn được thực thi, vì os.Exit bỏ qua các lời gọi đã defer.
Trong một HTTP server, tầng trên cùng là handler: nó ánh xạ lỗi sang mã trạng thái và một thông báo an toàn cho máy khách, rồi ghi log thông báo chi tiết cho bạn.
Lỗi bạn có thể bỏ qua, và lỗi không được bỏ qua
Bỏ qua một lỗi đôi khi là đúng, nhưng hãy làm điều đó một cách tường minh với _ để người đọc biết đây là một quyết định có chủ ý:
_ = conn.SetDeadline(t) // best effort
Một số lời gọi trên thực tế không thể thất bại (strings.Builder.WriteString, bytes.Buffer.Write). Một số khác trông vô hại nhưng không phải vậy: Close trên một tệp bạn vừa ghi có thể báo rằng dữ liệu chưa bao giờ tới được đĩa, và json.Marshal thất bại với channel và hàm. Khi không chắc chắn, hãy kiểm tra.
Linter errcheck (có trong golangci-lint) báo các lỗi chưa được kiểm tra. Bản thân go vet không đánh dấu chúng.
Lỗi và panic
Go cũng có panic, nhưng đó không phải một hệ thống ngoại lệ. Hãy dùng lỗi cho mọi thứ có thể hỏng trong quá trình hoạt động bình thường: đầu vào sai, thiếu tệp, lỗi mạng. Chỉ dùng panic cho bug (một trạng thái không thể xảy ra, một bất biến bị phá vỡ) và cho những lỗi lúc khởi động khiến việc chạy tiếp không còn ý nghĩa. Một thư viện gần như không bao giờ nên panic xuyên qua API của nó. Xem trang panic và recover.
Giảm lặp lại
if err != nil khá dài dòng, và các đề xuất thêm cú pháp mới cho nó đã nhiều lần bị từ chối; năm 2025, nhóm phát triển Go thông báo sẽ không theo đuổi thay đổi cú pháp cho xử lý lỗi nữa. Một vài mẫu giúp giảm bớt sự rườm rà ngay trong ngôn ngữ:
- Trả về sớm và giữ hàm ngắn gọn. Phần lớn sự lặp lại đến từ những hàm dài làm nhiều bước.
- Lỗi bám dính (sticky error). Với một chuỗi thao tác ghi, giữ lỗi đầu tiên trong một trường của struct và biến các lời gọi sau thành không làm gì khi lỗi đã được đặt.
bufio.Writerhoạt động theo cách này: bạn chỉ kiểm tra lỗi một lần sauFlush. - Wrap một lần cho mỗi hàm. Một closure được defer trên kết quả có tên có thể thêm cùng một ngữ cảnh cho mọi lỗi mà hàm trả về (xem defer).
Những lỗi thường gặp
- Dùng giá trị khi err khác nil. Kiểm tra trước, rồi mới dùng.
- Vừa ghi log vừa trả về. Chỉ chọn một.
- So sánh bằng
==sau khi wrap. Dùngerrors.Is. - Mất nguyên nhân vì dùng
%v. Dùng%w, trừ khi mục đích là che giấu nó. - Trả về một con trỏ nil có kiểu dưới dạng
error.var e *MyErr; return elà khác nil đối với bên gọi. Hãy trả vềniltrực tiếp. - Thông báo viết hoa hoặc có dấu câu.
errors.New("Failed to connect.")đọc rất kỳ khi bị wrap. Hãy viếtconnect to db: ....
Câu hỏi thường gặp
Xử lý lỗi trong Go hoạt động như thế nào?
Các hàm có thể thất bại sẽ trả về một error làm kết quả cuối cùng. Bên gọi kiểm tra nó ngay lập tức: v, err := f(); if err != nil { return err }. Một error là một giá trị interface bình thường với một phương thức duy nhất, Error() string, và nil nghĩa là thành công. Go không có ngoại lệ.
Go có try/catch không?
Không. Go không có ngoại lệ và không có try/catch. Những lỗi có thể lường trước được trả về dưới dạng giá trị error và được kiểm tra bằng if err != nil. panic và recover có tồn tại, nhưng chúng dành cho bug trong chương trình và các trạng thái không thể phục hồi, không phải cho luồng xử lý lỗi thông thường.
Làm sao trả về lỗi trong Go?
Khai báo error là kết quả cuối cùng và trả về nil khi thành công. Tạo lỗi bằng errors.New("message") cho nội dung cố định, hoặc fmt.Errorf("reading %s: %w", name, err) để thêm ngữ cảnh cho một lỗi bạn nhận được. Khi thất bại, trả về giá trị zero cho các kết quả còn lại.
%w và %v trong fmt.Errorf khác nhau thế nào?
Cả hai đều đưa thông báo của lỗi gốc vào lỗi mới. %w còn wrap lỗi gốc, nên errors.Is và errors.As vẫn tìm thấy nó. %v tạo một lỗi mới chỉ có phần văn bản. Dùng %w khi bên gọi có thể cần kiểm tra nguyên nhân, và %v khi bạn muốn che giấu nó.
Làm sao kiểm tra lỗi nào được trả về trong Go?
Dùng errors.Is(err, target) để so sánh với một lỗi sentinel như io.EOF hoặc os.ErrNotExist, và errors.As(err, &target) để lấy ra một kiểu lỗi cụ thể như *fs.PathError. Cả hai đều đi qua các lớp lỗi đã được wrap. Tránh so sánh chuỗi err.Error().