Go Zero to Hero - Bài 64: Giới thiệu package context và tầm quan trọng trong Microservices
1. Context là gì? Sợi dây sinh mệnh của Request
context (ngữ cảnh) trong Go là một đối tượng mang theo 2 thứ trên suốt chặng đường một Request đi qua các hàm và các Service:
- Tín hiệu điều khiển vòng đời (Cancellation/Timeout Signal): Báo cho các hàm biết khi nào thì phải dừng lại ngay lập tức.
- Dữ liệu siêu dữ liệu (Metadata): Chứa các thông tin định danh của Request (như
RequestID,UserID,UserToken) để phục vụ cho việc ghi Log và truy vết (Tracing).
Hãy tưởng tượng context giống như một "Lệnh bài khẩn cấp" được truyền tay nhau từ vị tướng (hàm main hoặc API Gateway) xuống các tiểu đội (các Goroutines con). Khi vị tướng hô "Rút lui!" (hủy context), lệnh bài này lập tức phát tín hiệu, và mọi tiểu đội dù đang làm dở việc gì cũng phải lập tức vứt vũ khí chạy về, giải phóng toàn bộ tài nguyên.
2. Tầm quan trọng tối thượng trong Microservices
Trong kiến trúc Monolithic (ứng dụng nguyên khối cũ), luồng xử lý khá ngắn. Nhưng trong Microservices, một Request thường xuyên phải đi qua một chuỗi Domino các dịch vụ mạng:
Sức mạnh của Context giải quyết 2 bài toán lớn nhất ở đây:
- Chặn đứng Cascading Failures (Hiệu ứng Domino đổ sụp): Nếu Payment Service bị treo, Ticket Service đứng chờ, dẫn đến API Gateway cũng đứng chờ. Hàng ngàn Request kẹt lại sẽ kéo sập toàn bộ hệ thống. Với context, bạn có thể đặt một giới hạn (Timeout): "Chỉ cho phép chuỗi Domino này chạy tối đa 3 giây. Quá 3 giây, tự động phát tín hiệu hủy diệt toàn bộ chuỗi!"
- Distributed Tracing (Truy vết phân tán): Làm sao để biết một dòng log lỗi ở Payment Service thuộc về User nào đang gọi ở API Gateway? Người ta nhét một cái
TraceIDvào context ở cổng vào, và truyền nó đi xuyên suốt các hệ thống.
3. Khởi nguồn của mọi Context
Mọi dòng thác đều bắt nguồn từ một giọt nước. Trong Go, mọi cây Context phức tạp đều phải bắt đầu từ một "Context gốc" (Root Context) rỗng. Go cung cấp 2 hàm để tạo ra nó:
package main
import (
"context"
"fmt"
)
func main() {
// 1. context.Background()
// Dùng ở tầng cao nhất (hàm main, hàm init, hoặc điểm bắt đầu của một request)
ctx1 := context.Background()
// 2. context.TODO()
// Dùng khi bạn đang code dở dang, chưa biết phải dùng context nào,
// hoặc cấu trúc hàm yêu cầu context nhưng bạn chưa có để truyền vào. (Dùng làm placeholder)
ctx2 := context.TODO()
fmt.Println(ctx1)
fmt.Println(ctx2)
}
Lưu ý: Cả
Background()vàTODO()đều trả về một Context rỗng: Không chứa dữ liệu, không có thời hạn (timeout), và không bao giờ bị hủy. Chúng chỉ đóng vai trò là "cái móng nhà" để từ đó bạn xây đắp các tính năng lên.
4. Luật thép của giới Gopher khi dùng Context
Khi bạn đã quyết định đưa context vào dự án, bạn BẮT BUỘC phải tuân thủ 2 đạo luật bất thành văn sau, nếu không muốn bị các Senior chê là code "có mùi" (Code smell):
Luật 1: Luôn là tham số ĐẦU TIÊN của hàm
Nếu một hàm có liên quan đến I/O (gọi Database, gọi API ngoại vi, xử lý tốn thời gian), biến context.Context phải luôn được đặt ở vị trí đầu tiên trong danh sách tham số, và theo quy ước luôn được đặt tên là ctx.
// ❌ SAI LUẬT:
func fetchUserData(userID string, dbConn *sql.DB, ctx context.Context) error { ... }
// ✅ CHUẨN MỰC QUỐC TẾ:
func fetchUserData(ctx context.Context, userID string, dbConn *sql.DB) error { ... }
Luật 2: Tuyệt đối KHÔNG nhét Context vào trong Struct
Nhiều người từ Java sang có thói quen nhét mọi thứ vào Struct để tạo Class. Nhưng Context sinh ra là để đại diện cho vòng đời của một Request cụ thể, chứ không phải vòng đời của một Đối tượng (Object).
// ❌ THẢM HỌA THIẾT KẾ:
type TicketService struct {
ctx context.Context // Không bao giờ làm điều này!
db *sql.DB
}
// ✅ THIẾT KẾ ĐÚNG: Struct chỉ chứa DB. Context được truyền vào từng hàm riêng biệt.
type TicketService struct {
db *sql.DB
}
func (s *TicketService) BookTicket(ctx context.Context, req BookRequest) error {
// ... sử dụng ctx ở đây
}
💡 Tổng kết
Bạn đã nắm được tư duy tổng quan về người gác cổng vòng đời của Go:
- Bản chất:
contextlà một gói tin mang theo Tín hiệu hủy (Cancellation) và Siêu dữ liệu (Metadata) chảy xuyên suốt các tầng hàm/dịch vụ. - Khởi tạo: Mọi thứ bắt đầu bằng
context.Background(). - Vị trí: Luôn là tham số truyền vào đầu tiên mang tên
ctx, không bao giờ được cất giữ làm thuộc tính của một Struct.
All rights reserved