
Context trong Go: truyền dữ liệu, deadline, cancel - dùng đúng hay chết hiệu năng
- Golang
- August 15, 2025
Table of Contents
Hồi mới code Go, mình có một bug production kỳ lạ: cứ sau 3-4 ngày chạy, service tự dưng ngốn 8GB RAM rồi OOM. Mò suốt chiều thứ Sáu mới phát hiện: một cái goroutine không bao giờ kết thúc vì mình quên truyền context xuống gRPC call. Mỗi request leak một goroutine, sau 100K request thì server “đội mồ” luôn.
Đó là lần đầu mình hiểu context.Context không phải mấy dòng code cho có. Dùng sai là chết thật. Dùng đúng thì request dừng gọn, goroutine sạch, hệ thống nhẹ nhàng.
Dưới đây là những gì mình học được sau vài lần “chết” vì context - tất cả có code Go chạy được.
![Context trong Go(https://blog-bucket.luandnh.com/images/covers/go-context.png)
Context làm đúng 3 việc - đừng biến nó thành cái túi rác
Trước khi đi vào code, phải rõ context để làm gì:
- Cancellation - báo cho goroutine dừng khi request bị huỷ
- Deadline/Timeout - giới hạn thời gian cho tác vụ
- Truyền metadata nhẹ - request ID, user ID, trace ID
Không phải: kho chứa object nặng, không phải dependency injection container. Mình từng thấy codebase nhét cả DB connection vào context. Khóc.
Một câu thần chú để tự kiểm tra: “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 function parameter. Đừng nhét vào context.
Truyền dữ liệu đúng cách với context.Value
package ctxkeys
type key string
const (
RequestID key = "request_id"
UserID key = "user_id"
Locale key = "locale"
)
Dùng typed key để tránh collision - string rỗng "" là recipe cho bug khó debug nhất mình từng gặp.
ctx = context.WithValue(ctx, ctxkeys.RequestID, reqID)
// Extract với type assertion - luôn check ok
if id, ok := ctx.Value(ctxkeys.UserID).(string); ok {
log.Printf("[%s Processing request", id)
}
3 nguyên tắc mình tuân thủ tuyệt đối:
- ✅ Metadata nhỏ (< 1KB): request_id, user_id, trace_id, locale
- ❌ Object nặng: DB pool, Redis client, config struct → truyền qua constructor
- ❌ Business data: order ID, product filter, query params → truyền qua function params
- ❌ Đọc
ctx.Valuetrong hot loop → extract ra biến local một lần, xong mới loop
Mình từng thấy code lấy ctx.Value trong vòng lặp 10K items. CPU profile cho thấy 15% thời gian nằm ở type assertion của ctx.Value. Extract ra ngoài loop, 15% biến mất.
Deadline và Timeout
// Timeout cho DB query
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM orders WHERE user_id = $1", userID)
Cái defer cancel() quan trọng hơn bạn nghĩ. Không gọi cancel → context leak → timer resource không được giải phóng cho đến khi timeout hết. Trong service nhận 1K req/s, mỗi cái leak 3 giây timer… tự tính đi.
Deadline từ upstream nên truyền xuống, không override:
func (s *Service) GetOrder(ctx context.Context, orderID string) (*Order, error) {
// KHÔNG làm thế này:
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// LÀM thế này - respect deadline từ caller:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.repo.GetOrder(ctx, orderID)
}
Nếu client đã set deadline 2s, gói thêm WithTimeout(ctx, 5s) thì deadline vẫn là 2s (lấy cái ngắn hơn). Nhưng nếu bạn dùng context.Background() làm parent - bạn đã cắt đứt chain và vô hiệu hóa deadline của client. Lỗi này mình mất 2 tiếng debug.
Cancel khi không cần kết quả nữa
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
select {
case <-ctx.Done():
return // cleanup, không leak
case result := <-ch:
process(result)
}
}()
Truyền context qua các layer
// gRPC handler
func (s *Server) GetMenu(ctx context.Context, req *pb.MenuRequest) (*pb.MenuResponse, error) {
// ctx được truyền xuyên suốt
merchant, err := s.merchantRepo.GetByID(ctx, req.MerchantId)
if err != nil {
return nil, err
}
items, err := s.menuService.ListItems(ctx, merchant.ID, req.Filters)
if err != nil {
return nil, err
}
return &pb.MenuResponse{Items: items}, nil
}
Errgroup: chạy song song, cancel đồng loạt
import "golang.org/x/sync/errgroup"
func fetchDashboard(ctx context.Context, userID string) (*Dashboard, error) {
g, ctx := errgroup.WithContext(ctx)
var users *UserList
var orders *OrderList
var stats *Stats
g.Go(func() error {
var err error
users, err = fetchUsers(ctx, userID)
return err
})
g.Go(func() error {
var err error
orders, err = fetchOrders(ctx, userID)
return err
})
g.Go(func() error {
var err error
stats, err = fetchStats(ctx, userID)
return err
})
if err := g.Wait(); err != nil {
return nil, err
}
return &Dashboard{users, orders, stats}, nil
}
Một goroutine lỗi → ctx cancel → 2 goroutine còn lại dừng sớm. Sạch.
Worker pool + context
func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result) {
for {
select {
case <-ctx.Done():
log.Printf("worker %d: shutting down", id)
return
case job, ok := <-jobs:
if !ok {
return
}
results <- process(job)
}
}
}
Trace: luôn extract từ context
func HandleRequest(ctx context.Context, req *Request) (*Response, error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "HandleRequest")
defer span.Finish()
// Truyền ctx xuống - trace ID tự động propagate
return s.service.Process(ctx, req)
}
Không cần extract trace ID thủ công rồi bỏ vào log - context làm việc đó tự động nếu bạn setup instrumentation đúng.
Test với context
func TestGetOrder_WithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := svc.GetOrder(ctx, "slow-order-123")
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
Cheatsheet
| Việc | Dùng | Không dùng |
|---|---|---|
| Request-scoped metadata | ✅ ctx.Value | ❌ Business data |
| Deadline/Timeout | ✅ context.WithTimeout | ❌ context.Background override |
| Cancel goroutine | ✅ ctx.Done() + select | ❌ Quên defer cancel() |
| Truyền qua layer | ✅ Function param | ❌ Global variable |
Bottom line
Context trong Go không khó. Cái khó là kỷ luật: luôn truyền context xuống, luôn gọi defer cancel(), không nhét rác vào context.Value, không override deadline từ upstream. Mình mất vài bug production mới nhớ mấy cái này. Hy vọng bạn đọc xong không phải trả giá như mình.
Bạn đã từng debug cái bug “goroutine leak do quên cancel context” bao giờ chưa? Hay team bạn có convention gì về context.Value không? Mình tò mò cách team khác handle cái này lắm - kể mình nghe với. 🦞