# Xử lý lỗi trong Go năm 2026: Các pattern, Error Wrapping và câu hỏi phỏng vấn kỹ thuật > Tổng hợp các pattern xử lý lỗi trong Go: sentinel errors, custom error types, errors.Is, errors.As, error wrapping với fmt.Errorf %w và các câu hỏi phỏng vấn thường gặp. - Published: 2026-06-15 - Updated: 2026-06-15 - Author: SharpSkill - Tags: go, error-handling, best-practices, interview - Reading time: 8 min --- Cơ chế xử lý lỗi là một trong những đặc trưng quan trọng nhất của Go, phân biệt ngôn ngữ này với phần lớn các ngôn ngữ lập trình phổ biến hiện nay. Thay vì sử dụng exception và cấu trúc try/catch, Go coi lỗi là giá trị (error values) — được trả về tường minh từ hàm và bắt buộc kiểm tra tại mỗi điểm gọi. Thiết kế có chủ đích này đưa việc xử lý lỗi ra phía trước, giúp các luồng thất bại trở nên rõ ràng và dễ kiểm thử. > **Error Values, không phải Exceptions** > > Go không có cơ chế exception. Mọi hàm có khả năng thất bại đều trả về một error làm giá trị trả về cuối cùng. Pattern này khiến chi phí của việc bỏ qua lỗi trở nên rõ ràng ngay tại mã nguồn — và giúp các luồng xử lý lỗi có thể kiểm thử một cách đơn giản. ## Interface error và tầm quan trọng của nó Toàn bộ hệ thống xử lý lỗi trong Go dựa trên một interface duy nhất được định nghĩa trong [thư viện chuẩn](https://pkg.go.dev/builtin#error): ```go // builtin.go type error interface { Error() string } ``` Bất kỳ kiểu dữ liệu nào triển khai phương thức `Error() string` đều thỏa mãn interface này. Sự đơn giản đó thúc đẩy khả năng kết hợp: error có thể là struct, wrapper, hoặc bất kỳ kiểu nào khác, miễn là chúng tạo ra một chuỗi biểu diễn. Một kiểu error tùy chỉnh cơ bản minh họa cho pattern này: ```go // apperror.go type NotFoundError struct { Resource string ID string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID) } ``` `NotFoundError` mang theo dữ liệu có cấu trúc. Phía gọi có thể trích xuất tên tài nguyên hoặc ID theo chương trình thay vì phải phân tích chuỗi — một lợi thế quan trọng khi xây dựng HTTP API hoặc công cụ CLI cần ánh xạ lỗi sang các phản hồi cụ thể. ## Sentinel Errors cho các điều kiện lỗi phổ biến Sentinel errors là các biến cấp package đại diện cho các điều kiện thất bại cụ thể, được biết trước. Thư viện chuẩn sử dụng pattern này rộng rãi: `io.EOF`, `sql.ErrNoRows`, `os.ErrNotExist`. ```go // errors.go var ( ErrNotFound = errors.New("record not found") ErrUnauthorized = errors.New("unauthorized access") ErrConflict = errors.New("resource conflict") ) ``` Sentinel errors hoạt động tốt nhất khi phía gọi chỉ cần biết *điều gì* đã thất bại, chứ không cần biết *tại sao* một cách chi tiết. Chúng báo hiệu một điều kiện mà không mang theo ngữ cảnh bổ sung. Quy ước trong Go là đặt tiền tố `Err` và giữ thông báo viết thường, không có dấu chấm câu, tuân theo [hướng dẫn Go Code Review Comments](https://go.dev/wiki/CodeReviewComments#error-strings). > **Khi nào Sentinel Errors trở thành vấn đề** > > Việc export sentinel errors tạo ra một hợp đồng API công khai. Các package phụ thuộc sẽ so khớp với chúng bằng `errors.Is`, do đó việc đổi tên hoặc xóa một sentinel error là thay đổi phá vỡ tương thích. Nên sử dụng chúng một cách tiết kiệm — chỉ cho những điều kiện mà phía gọi thực sự cần phân nhánh logic. ## Error Wrapping với fmt.Errorf và verb %w Error wrapping, được giới thiệu từ Go 1.13, thêm ngữ cảnh vào một error trong khi vẫn bảo toàn error gốc trong chuỗi. Verb `%w` trong `fmt.Errorf` tạo ra chuỗi này: ```go // repository.go func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) { user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id) if err != nil { return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err) } return user, nil } ``` Error sau khi wrap vẫn giữ nguyên toàn bộ chuỗi. Một hàm gọi cách ba tầng phía trên vẫn có thể so khớp với sentinel error gốc hoặc trích xuất kiểu error ban đầu. Mỗi tầng thêm ngữ cảnh về *nơi* lỗi xảy ra mà không che khuất *điều gì* đã xảy ra. Một điểm phân biệt quan trọng: `%w` wrap error (bảo toàn chuỗi), trong khi `%v` format error thành chuỗi ký tự và phá vỡ chuỗi liên kết. Nên sử dụng `%v` một cách có chủ đích khi error gốc không nên lộ ra qua ranh giới trừu tượng — ví dụ, khi repository wrap một error từ database driver mà code tầng service không bao giờ nên kiểm tra. ## Kiểm tra lỗi với errors.Is và errors.As Package `errors` cung cấp hai hàm để kiểm tra chuỗi error đã được wrap, thay thế cho phép so sánh trực tiếp và type assertion. `errors.Is` duyệt qua chuỗi để tìm một giá trị error cụ thể: ```go // handler.go func handleGetUser(w http.ResponseWriter, r *http.Request) { user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id")) if errors.Is(err, ErrNotFound) { http.Error(w, "User not found", http.StatusNotFound) return } if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) } ``` `errors.As` trích xuất một kiểu error cụ thể từ chuỗi: ```go // middleware.go func errorMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { http.Error(w, "Internal error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } func mapErrorToHTTP(err error) int { var notFound *NotFoundError if errors.As(err, ¬Found) { return http.StatusNotFound } var validationErr *ValidationError if errors.As(err, &validationErr) { return http.StatusBadRequest } return http.StatusInternalServerError } ``` Lợi thế chính so với type assertion: cả hai hàm đều duyệt qua toàn bộ chuỗi đã wrap. Một `NotFoundError` được wrap ba lần bằng `fmt.Errorf` và `%w` vẫn có thể được so khớp thành công. ## Xử lý lỗi có cấu trúc trong ứng dụng nhiều tầng Các ứng dụng Go trong môi trường production thường tổ chức error theo ba tầng: domain errors định nghĩa các điều kiện nghiệp vụ, service errors bổ sung ngữ cảnh vận hành, và [handler hoặc transport errors](https://www.datadoghq.com/blog/go-error-handling/) ánh xạ chúng sang các phản hồi bên ngoài. ```go // domain/errors.go type DomainError struct { Code string Message string Err error } func (e *DomainError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("%s: %s", e.Code, e.Message) } func (e *DomainError) Unwrap() error { return e.Err } ``` Phương thức `Unwrap` chính là thứ giúp `errors.Is` và `errors.As` hoạt động xuyên suốt chuỗi. Bất kỳ kiểu error tùy chỉnh nào wrap error khác đều nên triển khai phương thức này. Tầng service wrap domain errors với ngữ cảnh vận hành: ```go // service/user.go func (s *UserService) Deactivate(ctx context.Context, userID string) error { user, err := s.repo.FindByID(ctx, userID) if err != nil { return fmt.Errorf("deactivating user %s: %w", userID, err) } if user.Status == StatusInactive { return &DomainError{ Code: "ALREADY_INACTIVE", Message: fmt.Sprintf("user %s is already inactive", userID), } } return s.repo.UpdateStatus(ctx, userID, StatusInactive) } ``` ## Các câu hỏi phỏng vấn thường gặp về xử lý lỗi trong Go Các buổi phỏng vấn kỹ thuật cho vị trí Go developer thường xuyên kiểm tra kiến thức về xử lý lỗi. Những câu hỏi dưới đây xuất hiện phổ biến trong các vòng sàng lọc và được phân tích chi tiết tại [câu hỏi phỏng vấn xử lý lỗi Go](/technologies/go/interview-questions/error-handling). **Tại sao Go sử dụng error values thay vì exceptions?** Exceptions tạo ra luồng điều khiển ẩn. Một hàm throw exception chuyển quyền điều khiển đến một điểm catch không xác định, có thể cách nhiều stack frame. Error values trong Go giúp mọi luồng thất bại trở nên tường minh trong chữ ký hàm. Phía gọi quyết định ngay lập tức cách xử lý lỗi — thử lại, wrap, ghi log, hoặc lan truyền. Điều này loại bỏ vấn đề "throw bất ngờ" và giúp các luồng xử lý lỗi có thể đọc hiểu mà không cần công cụ hỗ trợ. **Sự khác nhau giữa `%w` và `%v` trong `fmt.Errorf` là gì?** `%w` wrap error, bảo toàn chuỗi cho `errors.Is` và `errors.As`. `%v` format error thành chuỗi ký tự, tạo ra một error mới không có liên kết ngược về error gốc. Sử dụng `%w` khi phía gọi cần kiểm tra nguyên nhân; sử dụng `%v` khi nguyên nhân là chi tiết triển khai không nên lộ ra qua ranh giới trừu tượng. **Khi nào một hàm nên trả về error so với sử dụng panic?** Panic được dành cho các tình huống thực sự không thể khôi phục: lỗi lập trình như truy cập ngoài chỉ số mảng, tham chiếu nil pointer, hoặc vi phạm bất biến (invariant) cho thấy chương trình có bug. Các lỗi có thể khôi phục — timeout mạng, bản ghi không tồn tại, đầu vào không hợp lệ — trả về error. Một quy tắc hữu ích: nếu điều kiện đó có thể xảy ra trong vận hành bình thường, hàm trả về error. Nếu nó có nghĩa là chương trình có bug, hàm gọi panic. **Phương thức `Unwrap` ảnh hưởng như thế nào đến việc kiểm tra chuỗi error?** Khi một kiểu error tùy chỉnh triển khai `Unwrap() error`, các hàm `errors.Is` và `errors.As` sẽ theo phương thức đó để duyệt qua chuỗi. Nếu không có `Unwrap`, chuỗi dừng lại tại error đó. Kể từ Go 1.20, error còn có thể triển khai `Unwrap() []error` để trả về nhiều error đã wrap, cho phép chuỗi error dạng cây phục vụ các trường hợp như tổng hợp lỗi validation. ## Các anti-pattern cần tránh khi xử lý lỗi Một số anti-pattern xuất hiện thường xuyên trong các codebase Go và trong [các buổi review code](/blog/go/go-design-patterns-essential-patterns-interview): **Bỏ qua lỗi một cách im lặng.** Blank identifier giúp điều này trở nên dễ dàng về mặt cú pháp nhưng nguy hiểm về mặt ngữ nghĩa: ```go // anti-pattern: silent error swallowing result, _ := riskyOperation() ``` Mọi error bị bỏ qua nên là một quyết định có chủ đích và được ghi chú — không bao giờ là lối tắt. **Ghi log rồi vẫn trả về.** Pattern này tạo ra các bản ghi log trùng lặp và gây nhầm lẫn cho phía gọi vì phía đó cũng sẽ xử lý error: ```go // anti-pattern: double handling if err != nil { log.Printf("operation failed: %v", err) return err } ``` Cách sửa: hoặc xử lý error (ghi log, trả về giá trị mặc định, thử lại) *hoặc* lan truyền nó. Không làm cả hai. **So khớp chuỗi để phân loại lỗi.** Kiểm tra đầu ra `err.Error()` bằng `strings.Contains` là cách làm dễ vỡ. Thông báo lỗi không phải là hợp đồng API — chúng có thể thay đổi giữa các phiên bản thư viện. Nên sử dụng `errors.Is` cho sentinel values và `errors.As` cho kiểu dữ liệu. **Wrap error xuyên ranh giới goroutine mà không đồng bộ hóa.** Khi nhiều [goroutine](/blog/go/go-concurrency-goroutines-channels) tạo ra error đồng thời, nên sử dụng `errgroup.Group` từ package `golang.org/x/sync` hoặc cơ chế đồng bộ hóa tương đương. Việc append trực tiếp vào một slice dùng chung mà không có mutex sẽ gây ra data race. ## Xử lý lỗi đồng thời với errgroup Package `errgroup` từ [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync/errgroup) cung cấp một pattern gọn gàng để thu thập error từ các goroutine chạy đồng thời: ```go // batch.go func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) for _, id := range orderIDs { g.Go(func() error { if err := s.processOrder(ctx, id); err != nil { return fmt.Errorf("processing order %s: %w", id, err) } return nil }) } return g.Wait() } ``` `errgroup.WithContext` hủy context dẫn xuất khi bất kỳ goroutine nào trả về error, báo hiệu cho các goroutine còn lại dừng lại. Cách tiếp cận này tránh lãng phí tài nguyên và cung cấp một error đã được wrap duy nhất cho phía gọi. > **Go 1.24 và xử lý lỗi** > > Go 1.24 (tháng 2 năm 2025) không thay đổi package errors. API xử lý lỗi đã ổn định kể từ Go 1.13, với bổ sung Go 1.20 cho phép unwrap nhiều error thông qua `Unwrap() []error`. Các đề xuất hiện tại để cải thiện cú pháp xử lý lỗi vẫn đang được thảo luận trong Go issue tracker. ## Kết luận - Coi error là giá trị: trả về tường minh và xử lý tại mỗi điểm gọi thay vì dựa vào cơ chế exception ẩn - Sử dụng sentinel errors (`var ErrX = errors.New(...)`) chỉ cho những điều kiện mà phía gọi cần phân nhánh logic, và coi chúng như API công khai - Wrap error bằng `fmt.Errorf` và `%w` để thêm ngữ cảnh trong khi bảo toàn chuỗi; dùng `%v` để chủ đích phá vỡ chuỗi tại ranh giới trừu tượng - Ưu tiên `errors.Is` thay vì `==` và `errors.As` thay vì type assertion — cả hai đều duyệt qua toàn bộ chuỗi đã wrap - Triển khai `Unwrap() error` trên các kiểu error tùy chỉnh để việc kiểm tra chuỗi hoạt động chính xác - Xử lý hoặc lan truyền — không bao giờ cả hai. Ghi log một error rồi trả về nó sẽ tạo ra nhiễu trùng lặp - Sử dụng `errgroup` để thu thập error đồng thời thay vì đồng bộ hóa goroutine thủ công - Giữ thông báo lỗi viết thường, không có dấu chấm câu, tuân theo quy ước Go --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/go/go-error-handling-patterns-wrapping-best-practices