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.

Context làm 3 việc - không hơn
- Huỷ (cancellation): báo cho các goroutine dừng việc dang dở.
- Deadline/Timeout: giới hạn thời gian cho tác vụ.
- 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.Valuetrở nên đắt và khó kiểm soát vòng đời tài nguyên. - Không truy cập
ctx.Valuetrong 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é! 👇