Go Deep Dive: Error Handling Patterns

Error handling là nơi Go khác biệt nhất so với các ngôn ngữ khác. Không có exceptions. Không có try/catch. Chỉ có values — và đó chính xác là ý định của họ.
Trong Phase 2 bạn đã học cơ bản: functions return (value, error) và bạn check if err != nil. Đúng rồi, nhưng đó chỉ là nền tảng. Trong production code bạn cần wrap errors để giữ context, inspect errors để đưa ra quyết định, và thiết kế custom error types mang thông tin có cấu trúc.
Deep dive này cover tất cả.
Bạn Sẽ Học Được Gì
✅ Tại sao Go chọn errors-as-values thay vì exceptions
✅ Sentinel errors và khi nào nên dùng
✅ Custom error types với thêm context
✅ Error wrapping với fmt.Errorf("%w", err)
✅ Unwrapping với errors.Is và errors.As
✅ Error chain và cách nó hoạt động bên trong
✅ Production patterns: logging, HTTP error responses, typed errors
✅ Những lỗi thường gặp và cách tránh
Part 1: Tại Sao Errors-as-Values?
Trong Python hay Java, bạn throw exception và nó unwind call stack một cách vô hình. Caller ở top của stack có thể catch nó — hoặc ignore nó — và compiler không quan tâm. Điều này dẫn đến các failure mode không được document.
Trong Go, mọi error là một return value tường minh. Compiler buộc bạn phải handle (hoặc tường minh ignore) errors:
// Interface `error` — tích hợp sẵn trong Go, chỉ hai chữ
type error interface {
Error() string
}
// Mọi function có thể thất bại đều return error ở cuối
func readFile(path string) ([]byte, error) {
// ...
}
// Caller PHẢI handle nó
data, err := readFile("config.json")
if err != nil {
// handle: log, wrap, return, hoặc panic — tùy bạn
return fmt.Errorf("đọc config: %w", err)
}Lợi ích:
- Failures hiển thị ngay tại call site — không có hidden control flow
- Static analysis tools có thể tìm unhandled errors (
errcheck,staticcheck) - Compiler không enforce việc handle, nhưng
go vetvà linters thì có
Cái giá: nhiều dòng code hơn. Go programmers chấp nhận điều này một cách tường minh. Trade-off về robustness xứng đáng.
Part 2: Ba Cách Tạo Errors
1. errors.New — Messages Đơn Giản, Tĩnh
import "errors"
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
func getUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("user ID phải là số dương")
}
// ...
}Dùng errors.New cho các one-off errors. Dùng package-level var declarations cho errors bạn sẽ check với errors.Is.
2. fmt.Errorf — Messages Có Format
import "fmt"
func getUser(id int) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("query user %d: %w", id, err)
}
if user == nil {
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}
return user, nil
}Verb %w wrap error gốc — giữ nguyên toàn bộ chain để unwrap sau này. Điều này cực kỳ quan trọng khi debug.
3. Custom Error Types — Dữ Liệu Có Cấu Trúc
Khi caller cần nhiều hơn một string, tạo một type:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("lỗi validation ở field %q: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "phải là số không âm"}
}
if age > 150 {
return &ValidationError{Field: "age", Message: "giá trị không thực tế"}
}
return nil
}Caller có thể inspect type bằng errors.As để truy cập các fields có cấu trúc như Field.
Part 3: Sentinel Errors
Sentinel error là một package-level error variable dùng để so sánh. Nó tương đương với một specific exception class trong Go:
package store
import "errors"
// Exported sentinel errors — callers có thể check với những cái này
var (
ErrNotFound = errors.New("not found")
ErrDuplicate = errors.New("duplicate entry")
ErrUnauthorized = errors.New("unauthorized")
)// Caller dùng errors.Is để check
user, err := store.GetUser(id)
if errors.Is(err, store.ErrNotFound) {
// Trả về 404 cho HTTP client
http.Error(w, "Không tìm thấy user", http.StatusNotFound)
return
}
if err != nil {
// Lỗi không rõ — trả về 500
http.Error(w, "Lỗi máy chủ nội bộ", http.StatusInternalServerError)
log.Printf("lỗi không mong đợi khi lấy user %d: %v", id, err)
return
}Các ví dụ từ standard library bạn đã dùng:
io.EOF— báo hiệu kết thúc streamsql.ErrNoRows— không có rows nào được trả về từ querycontext.Canceled,context.DeadlineExceeded— trạng thái context
Khi Nào Dùng Sentinel Errors
| Tình huống | Dùng sentinel? |
|---|---|
| Caller cần rẽ nhánh theo một lỗi cụ thể | ✅ Có |
| Lỗi là điều kiện phổ biến, được mong đợi (không phải bug) | ✅ Có |
| Lỗi là nội bộ của package | ❌ Không |
| Lỗi cần dữ liệu có cấu trúc | ❌ Không — dùng custom type thay thế |
Part 4: Error Wrapping — The Error Chain
Error wrapping là cách bạn thêm context mà không mất nguyên nhân gốc.
// Không wrap — error gốc bị mất
return nil, fmt.Errorf("thao tác thất bại") // 😞 caller không biết tại sao
// Có wrap — chain được giữ nguyên
return nil, fmt.Errorf("lưu user: %w", err) // ✅Verb %w tạo một error wrap err. Khi bạn in nó ra, bạn thấy toàn bộ message. Khi bạn unwrap nó, bạn lấy lại err.
Xây Dựng Một Chain
func ReadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("ReadConfig: đọc file %q: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("ReadConfig: parse JSON từ %q: %w", path, err)
}
return &cfg, nil
}
func StartServer(cfgPath string) error {
cfg, err := ReadConfig(cfgPath)
if err != nil {
return fmt.Errorf("StartServer: %w", err)
}
// ...
return nil
}Khi StartServer thất bại, error message sẽ là:
StartServer: ReadConfig: đọc file "config.json": open config.json: no such file or directoryMỗi layer đặt tên mình + wrap layer bên dưới. Đây là call stack được encode trong error string — cực kỳ quý giá khi debug production failures.
Part 5: errors.Is và errors.As
errors.Is — Kiểm Tra Toàn Bộ Chain
errors.Is(err, target) duyệt toàn bộ error chain tìm một match — không chỉ error ngoài cùng:
// Dù err được wrap nhiều lớp sâu,
// errors.Is vẫn tìm thấy ErrNotFound trong chain
err := fmt.Errorf("handler: %w", fmt.Errorf("store: %w", ErrNotFound))
errors.Is(err, ErrNotFound) // true ✅
// Cách cũ — chỉ check error ngoài cùng
err == ErrNotFound // false ❌ — wrapped errors không bằng nhauImplement Is cho custom types khi bạn muốn custom comparison logic:
type HTTPError struct {
Code int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
// Cho phép errors.Is match theo status code
func (e *HTTPError) Is(target error) bool {
t, ok := target.(*HTTPError)
if !ok {
return false
}
return t.Code == e.Code
}
// Sử dụng
var ErrNotFoundHTTP = &HTTPError{Code: 404}
err := fmt.Errorf("lấy profile: %w", &HTTPError{Code: 404, Message: "user not found"})
errors.Is(err, ErrNotFoundHTTP) // true ✅ — match theo Codeerrors.As — Trích Xuất Type Từ Chain
errors.As(err, &target) tìm error đầu tiên trong chain có thể gán cho target:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func processRequest(input Input) error {
if err := validate(input); err != nil {
return fmt.Errorf("processRequest: %w", err)
}
return nil
}
// Ở handler level:
err := processRequest(badInput)
var valErr *ValidationError
if errors.As(err, &valErr) {
// ✅ valErr được populate với ValidationError cụ thể
fmt.Printf("Field sai: %s\n", valErr.Field)
http.Error(w, fmt.Sprintf("Invalid %s: %s", valErr.Field, valErr.Message), http.StatusBadRequest)
return
}errors.As tìm kiếm trong chain — dù ValidationError bị chôn vùi dưới nhiều lớp fmt.Errorf("%w").
errors.Is vs errors.As
errors.Is | errors.As | |
|---|---|---|
| Kiểm tra | Identity hoặc equality | Type assignability |
| Target | Một giá trị error cụ thể | Con trỏ tới một error type |
| Dùng khi | Sentinel errors | Custom error types có fields |
| Ví dụ | errors.Is(err, io.EOF) | errors.As(err, &valErr) |
Part 6: Unwrap Thủ Công
errors.Unwrap(err) trả về wrapped error, hoặc nil nếu không có:
wrapped := fmt.Errorf("outer: %w", io.EOF)
inner := errors.Unwrap(wrapped)
fmt.Println(inner) // EOFVới custom types wrap errors, implement Unwrap():
type AppError struct {
Code string
Message string
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Implement Unwrap để errors.Is/As hoạt động xuyên qua type này
func (e *AppError) Unwrap() error {
return e.Cause
}Bây giờ errors.Is và errors.As có thể traverse qua custom type của bạn.
Part 7: Production Error Patterns
Pattern 1 — HTTP Error Responses
Map errors thành HTTP status codes ở boundary (trong handler), không phải sâu trong business logic:
type AppError struct {
Code string // machine-readable code
Message string // human-readable message
HTTPCode int // HTTP status code tương ứng
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Cause }
// Package-level constructors
func NotFound(msg string, cause error) *AppError {
return &AppError{Code: "NOT_FOUND", Message: msg, HTTPCode: 404, Cause: cause}
}
func BadRequest(msg string, cause error) *AppError {
return &AppError{Code: "BAD_REQUEST", Message: msg, HTTPCode: 400, Cause: cause}
}
func Internal(msg string, cause error) *AppError {
return &AppError{Code: "INTERNAL", Message: msg, HTTPCode: 500, Cause: cause}
}// Handler
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
user, err := userService.GetByID(r.Context(), id)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
writeError(w, appErr.HTTPCode, appErr.Code, appErr.Message)
} else {
log.Printf("lỗi không mong đợi: %v", err)
writeError(w, 500, "INTERNAL", "Có gì đó sai rồi")
}
return
}
writeJSON(w, 200, user)
}Pattern 2 — Sentinel + Wrapping Qua Các Layers
// store/user.go
package store
var ErrNotFound = errors.New("not found")
func (s *Store) GetUser(ctx context.Context, id int) (*User, error) {
var user User
err := s.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound) // wrap sentinel
}
if err != nil {
return nil, fmt.Errorf("query user %d: %w", id, err)
}
return &user, nil
}// service/user.go
package service
func (s *Service) GetProfile(ctx context.Context, userID int) (*Profile, error) {
user, err := s.store.GetUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("GetProfile: %w", err) // giữ nguyên chain
}
// ...
return profile, nil
}// handler — check sentinel xuyên qua chain
profile, err := svc.GetProfile(r.Context(), id)
if errors.Is(err, store.ErrNotFound) {
http.Error(w, "Không tìm thấy profile", 404)
return
}store.ErrNotFound vẫn thấy được qua nhiều lớp wrapping. Business logic giữ sạch; handler quyết định cách respond.
Pattern 3 — Multi-Error (Nhiều Validations)
Khi cần thu thập nhiều errors cùng lúc:
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
msgs := make([]string, len(m.Errors))
for i, e := range m.Errors {
msgs[i] = e.Error()
}
return strings.Join(msgs, "; ")
}
func (m *MultiError) Unwrap() []error {
return m.Errors // Go 1.20+: trả về slice để errors.Is/As duyệt qua
}
func validateUser(u User) error {
var errs MultiError
if u.Name == "" {
errs.Errors = append(errs.Errors, &ValidationError{Field: "name", Message: "bắt buộc"})
}
if u.Email == "" {
errs.Errors = append(errs.Errors, &ValidationError{Field: "email", Message: "bắt buộc"})
}
if !isValidEmail(u.Email) {
errs.Errors = append(errs.Errors, &ValidationError{Field: "email", Message: "format không hợp lệ"})
}
if len(errs.Errors) > 0 {
return &errs
}
return nil
}Go 1.20+ giới thiệu errors.Join cho pattern này:
var errs []error
if u.Name == "" {
errs = append(errs, fmt.Errorf("name: bắt buộc"))
}
if u.Email == "" {
errs = append(errs, fmt.Errorf("email: bắt buộc"))
}
return errors.Join(errs...) // trả về nil nếu errs rỗngPart 8: panic và recover — Khi Nào Dùng
panic không phải là exception handling. Chỉ dùng cho:
- Programming errors (nil pointer dereference, out-of-bounds index)
- Những tình huống thực sự không thể recover được lúc startup (thiếu required env vars, config bị hỏng)
- Khởi tạo package-level state phải thành công
// ✅ Panic phù hợp — thiếu required env var khi startup
func mustGetEnv(key string) string {
v := os.Getenv(key)
if v == "" {
panic(fmt.Sprintf("biến môi trường bắt buộc %q chưa được set", key))
}
return v
}
var dbURL = mustGetEnv("DATABASE_URL") // fail fast khi startup// ✅ Recover phù hợp — trong HTTP middleware để survive panics
func panicRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered từ panic: %v\n%s", r, debug.Stack())
http.Error(w, "Lỗi máy chủ nội bộ", 500)
}
}()
next.ServeHTTP(w, r)
})
}// ❌ Không phù hợp — đừng dùng panic như flow control
func getUser(id int) *User {
if id <= 0 {
panic("ID không hợp lệ") // ❌ return error thay vào đây
}
}Quy tắc: Nếu failure có thể xảy ra trong production do điều kiện bên ngoài (input xấu, network failure, resource thiếu), return một error. Nếu nó chỉ ra một programming bug không bao giờ xảy ra trong code đúng, panic là chấp nhận được.
Part 9: Những Lỗi Thường Gặp
1. Bỏ Qua Errors
// ❌ Ignore errors im lặng
file, _ := os.Open("config.json")
defer file.Close()
// ✅ Xử lý hoặc tường minh document tại sao bỏ qua
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("mở config: %w", err)
}
defer file.Close()Dùng _ để discard errors chỉ khi bạn đã quyết định có ý thức rằng failure không quan trọng và document lý do.
2. Wrap Sentinels Sai Cách
// ❌ Dùng fmt.Errorf không có %w — tạo error mới, phá chain
return fmt.Errorf("not found: %v", ErrNotFound) // errors.Is sẽ không hoạt động!
// ✅ Dùng %w để wrap
return fmt.Errorf("user %d: %w", id, ErrNotFound)3. Wrap Errors Quá Nhiều Lần
// ❌ Thêm context ở mọi single level tạo ra noise
func a() error { return fmt.Errorf("a: %w", b()) }
func b() error { return fmt.Errorf("b: %w", c()) }
func c() error { return fmt.Errorf("c: %w", d()) }
func d() error { return errors.New("original") }
// Kết quả: "a: b: c: original" — lồng nhau quá nhiềuWrap khi bạn đang vượt qua một boundary có ý nghĩa (package, service, I/O operation) — không phải ở mỗi function call.
4. So Sánh Errors Bằng ==
// ❌ So sánh trực tiếp bị vỡ khi có wrapping
if err == io.EOF { ... } // thất bại nếu err bị wrap
// ✅ Luôn dùng errors.Is
if errors.Is(err, io.EOF) { ... } // duyệt qua chain5. Trả Về Generic Errors Từ Public APIs
// ❌ Caller không phân biệt được các cases
func (s *Store) GetUser(id int) (*User, error) {
return nil, errors.New("error") // lỗi gì vậy?
}
// ✅ Trả về sentinel hoặc typed errors từ public APIs
func (s *Store) GetUser(id int) (*User, error) {
// ...
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}Tổng Kết và Những Điều Cần Nhớ
Tạo errors:
✅ errors.New("msg") cho error values đơn giản, tĩnh
✅ fmt.Errorf("context: %w", err) để thêm context và wrap
✅ Custom types với Error() string khi cần dữ liệu có cấu trúc
Kiểm tra errors:
✅ errors.Is(err, target) — check với một error cụ thể xuyên qua chain
✅ errors.As(err, &typedErr) — trích xuất một error type cụ thể từ chain
✅ Không bao giờ dùng == để so sánh errors — wrapping phá vỡ nó
Wrapping discipline:
✅ Wrap tại các boundary có ý nghĩa: package edges, I/O, external calls
✅ Đặt tên context: fmt.Errorf("FunctionName: đang làm X: %w", err)
✅ Implement Unwrap() error trong custom types để chain có thể traverse được
Production:
✅ Định nghĩa sentinel errors trong packages cho các expected failure modes
✅ Dùng AppError hoặc tương tự ở HTTP boundaries để map errors thành status codes
✅ Dùng errors.Join (Go 1.20+) hoặc MultiError để thu thập nhiều failures
✅ Dùng panic chỉ cho programming bugs, không bao giờ cho expected runtime failures
Bước Tiếp Theo
Tiếp tục với các Go deep dives:
- Deep Dive: Concurrency với Goroutines & Channels — advanced patterns, fan-out/fan-in, pipelines
- Deep Dive: Interfaces & Polymorphism — empty interface, type assertions, interface design
- Deep Dive: Testing trong Go — testify, mocks, integration tests
- Deep Dive: Context, HTTP & Building APIs — middleware, real REST APIs
Các bài companion:
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.