0

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:

API Gateway→Auth Service→Ticket Service→Payment Service→Database\text{API Gateway} \rightarrow \text{Auth Service} \rightarrow \text{Ticket Service} \rightarrow \text{Payment Service} \rightarrow \text{Database}

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 TraceID và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: context là 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí