0

Bài 8: Quản lý vòng đời với context.Context

Tính năng mạnh mẽ và đặc trưng nhất của hệ sinh thái Go chính là việc quản lý vòng đời của một tác vụ thông qua context.Context.

Trong lập trình Web Backend, nếu một Client (người dùng) đóng trình duyệt hoặc ngắt kết nối mạng khi Server đang xử lý (ví dụ: đang gọi một query database tốn 10 giây), việc server tiếp tục xử lý và chạy query đó là hoàn toàn lãng phí tài nguyên. context.Context ra đời để giải quyết bài toán này.

1. Context trong HTTP Request là gì?

Mỗi đối tượng *http.Request trong Go luôn mang theo một Context ngầm định. Bạn có thể lấy nó ra bằng hàm r.Context().

Context này gắn liền với vòng đời của kết nối TCP.

  • Nếu request thành công, context tồn tại bình thường.

  • Nếu Client ngắt kết nối đột ngột (Cancel) hoặc quá thời gian (Timeout), tín hiệu Hủy (Cancellation Signal) sẽ được phát đi ngay lập tức thông qua Context này.

2. Bắt tín hiệu ngắt kết nối từ Client

Làm sao để Server biết Client đã bỏ đi để dừng các xử lý tốn kém? Chúng ta sử dụng hàm <-ctx.Done().

Hàm Done() trả về một channel. Channel này sẽ "mở" (phát ra tín hiệu) ngay khi request bị hủy. Hãy xem ví dụ một API cần 3 giây để xử lý:

Go

package main

import (
	"fmt"
	"net/http"
	"time"
)

func longRunningHandler(w http.ResponseWriter, r *http.Request) {
	// Lấy context từ request
	ctx := r.Context()

	fmt.Println("Bắt đầu xử lý tác vụ nặng...")

	// Dùng select để lắng nghe đồng thời 2 sự kiện:
	// 1. Tác vụ hoàn thành sau 3 giây (time.After)
	// 2. Client hủy request (ctx.Done())
	select {
	case <-time.After(3 * time.Second):
		// Giả lập query database thành công sau 3 giây
		fmt.Println("Xử lý xong!")
		w.Write([]byte("Dữ liệu của bạn đây."))

	case <-ctx.Done():
		// Bắt được tín hiệu Client đã đóng trình duyệt/hủy kết nối
		err := ctx.Err() // Lấy nguyên nhân lỗi (ví dụ: context canceled)
		fmt.Println("Ngừng xử lý vì Client đã ngắt kết nối:", err)
		// Không cần w.Write vì client không còn ở đó để nhận
	}
}

func main() {
	http.HandleFunc("/slow", longRunningHandler)
	http.ListenAndServe(":8080", nil)
}

Cách test: Mở trình duyệt gọi localhost:8080/slow, sau đó nhấn F5 (Reload) liên tục trước khi 3 giây kết thúc. Bạn sẽ thấy Terminal server in ra thông báo "Ngừng xử lý vì Client đã ngắt kết nối" ngay lập tức, tiết kiệm tài nguyên hệ thống đáng kể.

3. Chủ động tạo Context Timeout khi gọi API ngoài

Ở Bài 3, chúng ta đã biết cách set Timeout trên toàn bộ http.Client. Tuy nhiên, với context, bạn có thể set giới hạn thời gian cho từng Request cụ thể bằng cách sử dụng context.WithTimeout.

Go

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func main() {
	// Tạo một Background context và bọc nó bằng một Timeout 2 giây
	// Hàm cancel được trả về ĐỂ CHÚNG TA GỌI DỌN DẸP TÀI NGUYÊN nếu request hoàn thành sớm hơn 2s
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // LUÔN LUÔN defer cancel() để tránh memory leak

	// Tạo request kèm theo Context vừa tạo
	req, err := http.NewRequestWithContext(ctx, "GET", "https://httpstat.us/200?sleep=5000", nil)
	if err != nil {
		panic(err)
	}

	// API trên cố tình delay 5 giây, nhưng context của chúng ta chỉ cho phép 2 giây.
	client := &http.Client{}
	resp, err := client.Do(req)
	
	if err != nil {
		// Sẽ rơi vào lỗi này: "context deadline exceeded"
		fmt.Println("Yêu cầu thất bại:", err)
		return
	}
	defer resp.Body.Close()

	fmt.Println("Thành công!")
}

4. Truyền dữ liệu xuyên Middleware bằng Context

Ngoài việc quản lý vòng đời, Context còn đóng vai trò là một "chiếc cặp táp" lưu trữ dữ liệu cục bộ cho riêng request đó (Request-scoped values).

Ứng dụng thực tế nhất: Auth Middleware xác thực Token thành công, lấy ra được UserID của người dùng, và muốn truyền UserID này vào Main Handler.

Go

// 1. Định nghĩa một type riêng biệt cho Key của Context để tránh xung đột (Collision)
type contextKey string
const userIDKey contextKey = "userID"

// 2. Auth Middleware
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Giả sử decode Token ra được userID là "user-123"
		userID := "user-123"

		// Sao chép Context cũ, nhét thêm cặp Key-Value vào tạo thành Context mới
		ctx := context.WithValue(r.Context(), userIDKey, userID)

		// Thay thế Context cũ trong Request bằng Context mới
		reqWithCtx := r.WithContext(ctx)

		// Chuyển tiếp request ĐÃ CÓ DATA cho handler tiếp theo
		next.ServeHTTP(w, reqWithCtx)
	})
}

// 3. Main Handler
func profileHandler(w http.ResponseWriter, r *http.Request) {
	// Trích xuất dữ liệu từ Context bằng Key đã định nghĩa
	// Phải ép kiểu (Type Assertion) vì Value trả về kiểu 'any'
	userID, ok := r.Context().Value(userIDKey).(string)
	if !ok {
		http.Error(w, "Không tìm thấy UserID", http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Xin chào User: %s", userID)
}

Nguyên tắc vàng: Chỉ dùng context.WithValue cho dữ liệu đặc trưng của request (như User ID, Trace ID cho logging). Không bao giờ dùng nó để truyền các tham số bắt buộc của hàm (như truyền struct Database Connection).


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í