Go Zero to Hero - Bài 66: Quản lý timeout với context.WithTimeout và Deadline
1. context.WithTimeout: Quả bom hẹn giờ (Timer)
Hàm context.WithTimeout hoạt động chính xác như một chiếc đồng hồ đếm ngược. Bạn cấp cho nó một khoảng thời gian (Duration), ví dụ: 3 giây.
- Nếu sau 3 giây mà công việc chưa xong, Context sẽ tự động phát tín hiệu hủy (tương đương với việc ai đó tự động bấm hàm
cancel()). - Hàm này cũng trả về một biến
cancelFunc. Nó cho phép bạn chủ động hủy sớm hơn thời gian quy định nếu bạn muốn.
Cú pháp:
// Khởi tạo một Context sẽ tự động phát nổ sau 3 giây
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
Luật sinh tử cực kỳ quan trọng: Đừng nghĩ rằng "đằng nào nó cũng tự hủy sau 3 giây nên không cần gọi cancel()". Nếu API của bạn xử lý nhanh và chạy xong chỉ trong 0.5 giây, cái đồng hồ đếm ngược kia vẫn tiếp tục chạy ngầm trong RAM suốt 2.5 giây còn lại!
👉 Bắt buộc: Vẫn phải luôn kẹp
defer cancel()ngay bên dưới dòng khởi tạo để hủy bỏ cái đồng hồ ngay khi hàm kết thúc.
2. context.WithDeadline: Hẹn giờ tử hình (Alarm)
Nếu WithTimeout là "đếm ngược 3 giây nữa", thì WithDeadline là "đúng 12:00 đêm nay sẽ nổ".
- Thay vì nhận vào một khoảng thời gian (
time.Duration), nó nhận vào một thời điểm cụ thể (time.Time). - Thực chất, bên dưới mã nguồn của Golang,
WithTimeoutchỉ là một hàm bọc lại (wrapper) củaWithDeadlinebằng phép tính:thời_điểm_hiện_tại + khoảng_thời_gian.
Cú pháp:
// Hủy vào đúng 11:59:59 PM hôm nay
deadline := time.Date(2026, 8, 15, 23, 59, 59, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
Mẹo thực chiến: 95% thời gian làm Backend, bạn sẽ dùng
WithTimeout(cho các luồng API hoặc truy vấn DB).WithDeadlinechỉ thường được dùng cho các Cronjob chạy ngầm cần kết thúc trước một giờ cố định (ví dụ: Job đối soát vé cuối ngày phải xong trước 00:00).
3. Thực chiến: API trừ tiền tự cắt kết nối khi quá tải
Hãy xem cách chúng ta bảo vệ cổng AFC khỏi sự cố treo kết nối mạng khi gọi sang Server trung tâm:
package main
import (
"context"
"fmt"
"time"
)
// Hàm giả lập việc gọi API sang Server Trung Tâm (có thể bị lag)
func callCentralServer(ctx context.Context, cardID string) {
// Kênh dùng để báo cáo khi việc xử lý xong
done := make(chan bool)
// Tung luồng làm việc thực sự
go func() {
fmt.Printf("[Network] Đang gửi mã thẻ %s lên Server Trung Tâm...\n", cardID)
// Giả lập mạng bị lag cục bộ, mất 5 giây mới phản hồi
time.Sleep(5 * time.Second)
done <- true
}()
// LUỒNG CHÍNH ĐỨNG CHỜ: Một là Xong, Hai là Timeout
select {
case <-done:
fmt.Println("✅ [Thành công] Server đã trừ tiền, mở cổng!")
case <-ctx.Done(): // Lắng nghe tín hiệu từ bom hẹn giờ
fmt.Println("🚨 [TIMEOUT] Server phản hồi quá chậm! Đã ngắt kết nối.")
// Lấy lý do hủy: context.DeadlineExceeded
fmt.Println("Lý do:", ctx.Err())
}
}
func main() {
fmt.Println("--- KHÁCH HÀNG QUẸT THẺ ---")
// Đặt hạn chót: Nếu quá 3 giây không phản hồi, tự động ngắt!
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // Xóa đồng hồ đếm ngược sau khi hàm main kết thúc
callCentralServer(ctx, "AFC-VIP-9999")
fmt.Println("--- CỔNG XOAY TRỞ VỀ TRẠNG THÁI CHỜ ---")
}
Phân tích kết quả: Vì mạng lag tốn 5 giây, nhưng context chỉ cho phép 3 giây, nên nhánh
<-ctx.Done()sẽ nổ trước. Hệ thống lập tức ngắt luồng chờ và nhả tài nguyên, giữ cho cổng AFC không bị đơ.
4. Đặc quyền của Context: Tích hợp sẵn vào mọi thư viện Lõi
Bạn có thể thắc mắc: "Ở ví dụ trên, luồng Goroutine ngầm tải dữ liệu (time.Sleep(5s)) vẫn tiếp tục chạy dù nhánh select đã thoát ra, thế thì vẫn tốn RAM chứ?"
Đúng! Tự bản thân Context không thể phép thuật chui vào tắt cái Goroutine đó được, trừ phi bạn viết logic bắt <-ctx.Done() bên trong cái Goroutine ngầm đó.
👉 NHƯNG ĐÂY MỚI LÀ ĐIỀU KỲ DIỆU: Mọi thư viện chuẩn của Go (
net/http,database/sql) và các thư viện ORM/Cache nổi tiếng (GORM, Redis) đều đã viết sẵn logic lắng nghectx.Done()ở sâu bên trong lõi của chúng!Bạn chỉ cần truyền
ctxvào, phần còn lại (ngắt kết nối TCP, hủy truy vấn Postgres) bọn chúng sẽ tự động làm sạch sẽ!
// 1. Tự động hủy HTTP Request nếu API bên thứ 3 treo
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.vnpay.vn/check", nil)
client := &http.Client{}
resp, err := client.Do(req) // Hàm Do() sẽ bị ngắt cái rụp sau 3s!
// 2. Tự động hủy lệnh Query xuống PostgreSQL nếu Database bị Lock
// (Sử dụng với GORM, Prisma hoặc sql thuần)
db.WithContext(ctx).Exec("UPDATE tickets SET status = 'PAID' WHERE id = 1")
// 3. Tự động ngắt kết nối Redis
redisClient.Set(ctx, "session:123", "active", time.Hour).Err()
Đây chính là lý do vì sao Nguyên tắc thiết kế yêu cầu bạn phải luôn truyền Context từ trên cùng xuống dưới cùng của chuỗi hàm. Lệnh bài tử hình sẽ được chuyển phát thẳng xuống tận tầng Database/Network!
💡 Tổng kết
Chỉ với vài dòng lệnh, bạn đã xây dựng được lớp phòng thủ vững chãi nhất cho kiến trúc Microservices:
- Bom hẹn giờ: Dùng
context.WithTimeoutđể giới hạn thời gian sinh tồn của một Request/Truy vấn. - Bảo vệ RAM: Luôn dùng
defer cancel()để tắt cái đồng hồ đếm ngược khi mọi việc đã xong xuôi (tránh Context Leak). - Tích hợp: Sức mạnh thực sự của Context tỏa sáng khi bạn truyền nó vào các hàm chuẩn của Go như
http.NewRequestWithContexthoặc các lệnh Query Database. Chúng sẽ tự động thu hồi kết nối vật lý ngay khi Timeout xảy ra.
All rights reserved