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.

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ử.
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:
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:
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.
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.
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:
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ể:
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:
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 ánh xạ chúng sang các phản hồi bên ngoài.
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:
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)
}Sẵn sàng chinh phục phỏng vấn Go?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
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:
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:
// 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:
// 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 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 cung cấp một pattern gọn gàng để thu thập error từ các goroutine chạy đồng thời:
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 (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.Errorfvà%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.Isthay vì==vàerrors.Asthay vì type assertion — cả hai đều duyệt qua toàn bộ chuỗi đã wrap - Triển khai
Unwrap() errortrê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
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Design Pattern trong Go: Các pattern thiết yếu và câu hỏi phỏng vấn cho lập trình viên Go
Nắm vững các design pattern Go: Functional Options, Strategy, Factory và Observer. Ví dụ mã thực tế, thực hành tốt nhất theo phong cách idiomatic và các câu hỏi phỏng vấn thường gặp cho lập trình viên Go.

Go 1.26 Phỏng Vấn Kỹ Thuật: Green Tea GC, go fix và Tối Ưu Hóa Stack
Chuẩn bị phỏng vấn Go 1.26: Green Tea garbage collector giảm 10-40% overhead GC, go fix với modernizers, tối ưu slice trên stack, phát hiện rò rỉ goroutine và mật mã hậu lượng tử. Kèm ví dụ code và câu trả lời mẫu.

Top 25 câu hỏi phỏng vấn Go: hướng dẫn dành cho nhà phát triển
Chinh phục buổi phỏng vấn Go với 25 câu hỏi được hỏi nhiều nhất. Goroutine, channel, interface và mẫu đồng thời kèm ví dụ mã.