Contents

Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng

Mở đầu

context.Context là mảnh ghép nhỏ nhưng ảnh hưởng lớn tới độ ổn định và hiệu năng của dịch vụ Go. Dùng đúng: request dừng đúng lúc, hệ thống nhẹ nhàng. Dùng sai: rò rỉ goroutine, deadline chồng chéo, context.Value bị lạm dụng, CPU nhảy vọt.

Bài này tóm gọn cách tôi áp dụng context trong dự án backend: truyền dữ liệu gì, đặt deadline/timeout/cancel ở đâu, những lỗi làm chết hiệu năng, kèm ví dụ thực tế theo ngữ cảnh quán cà phê/đồ ăn để dễ hình dung.

https://blog-bucket.luandnh.com/images/covers/go-context.png

Context làm 3 việc - không hơn

  1. Huỷ (cancellation): báo cho các goroutine dừng việc dang dở.
  2. Deadline/Timeout: giới hạn thời gian cho tác vụ.
  3. Truyền dữ liệu nhẹ, cross-cutting: metadata kiểu request ID, user ID, locale. Không phải kho dữ liệu lưu mọi thứ.

Hãy luôn tự hỏi: “Giá trị này có thực sự là ngữ cảnh thực thi hay chỉ là tham số nghiệp vụ?” Nếu là tham số nghiệp vụ, truyền qua tham số hàm/struct; đừng nhét vào context.

Truyền dữ liệu đúng cách với context.Value

  • Chỉ dùng cho metadata nhỏ: request_id, user_id, trace_id, locale, permissions tóm tắt.
  • Dùng key kiểu riêng để tránh xung đột:
package ctxkeys

type key string

const (
    RequestID key = "request_id"
    UserID    key = "user_id"
)

Sử dụng:

ctx = context.WithValue(ctx, ctxkeys.RequestID, reqID)
if id, ok := ctx.Value(ctxkeys.UserID).(string); ok {
    // ...
}
  • Không bỏ object nặng (struct lớn, connection, repository, config toàn cục) vào context. Điều này khiến mọi ctx.Value trở nên đắt và khó kiểm soát vòng đời tài nguyên.
  • Không truy cập ctx.Value trong vòng lặp nóng (hot loop). Nếu cần, rút giá trị ra một lần rồi truyền xuống như biến cục bộ.

Deadline và Timeout

Timeout cho từng request

ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

result, err := db.QueryContext(ctx, query)

Deadline từ client nên truyền xuống

Nếu client gửi deadline, hãy truyền nó xuống tầng service và DB. Đừng tự ý override bằng timeout cứng.

Cancel khi không cần kết quả

ctx, cancel := context.WithCancel(ctx)
defer cancel()

go func() {
    select {
    case <-ctx.Done():
        return
    case result := <-ch:
        process(result)
    }()
}()

Client side: truyền context xuống

Khi gọi gRPC/HTTP từ một service khác, luôn truyền context xuống:

func (s *Server) HandleRequest(ctx context.Context, req *Request) (*Response, error) {
    // Truyền ctx xuống DB, Redis, gRPC calls...
    user, err := s.userRepo.GetByID(ctx, req.UserID)
    if err != nil {
        return nil, err
    }
    return s.processUser(ctx, user)
}

Errgroup với context

g, ctx := errgroup.WithContext(ctx)

g.Go(func() error {
    return fetchUsers(ctx)
})

g.Go(func() error {
    return fetchOrders(ctx)
})

if err := g.Wait(); err != nil {
    return err
}

Khi một goroutine trả về lỗi, context bị cancel → các goroutine còn lại dừng sớm.

Worker pool với context

func worker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
    for {
        select {
        case <-ctx.Done():
            return
        case job, ok := <-jobs:
            if !ok {
                return
            }
            results <- process(job)
        }
    }
}

Trace và Context

Luôn extract trace từ context:

span, ctx := opentracing.StartSpanFromContext(ctx, "HandleRequest")
defer span.Finish()

Test với context

func TestService(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    result, err := svc.DoSomething(ctx, input)
    require.NoError(t, err)
    assert.Equal(t, expected, result)
}

Tóm tắt

Việc Dùng Không dùng
Metadata nhẹ ctx.Value ❌ Object nặng
Deadline/Timeout ✅ Truyền xuống ❌ Override cứng
Cancel defer cancel() ❌ Quên cancel
Tham số nghiệp vụ ctx.Value ✅ Tham số hàm

Context là công cụ mạnh nhưng cần dùng đúng cách. Hy vọng bài viết giúp bạn tránh những lỗi phổ biến khi làm việc với context.Context trong Go.


Bạn thấy context dùng sai ở đâu trong dự án của mình? Comment bên dưới nhé! 👇