Go Context Package 2026: Hủy bỏ, Timeout và Câu hỏi Phỏng vấn

Làm chủ package context Go để hủy bỏ, timeout và request-scoped values. Hướng dẫn đầy đủ với ví dụ code và câu hỏi phỏng vấn.

Go Context Package 2026: Hủy bỏ, Timeout và Câu hỏi Phỏng vấn

Package context của Go cung cấp cơ chế tiêu chuẩn để mang theo deadline, tín hiệu hủy bỏ và request-scoped values qua các ranh giới API. Mọi ứng dụng Go production đều sử dụng context: HTTP handler, truy vấn database, gọi gRPC và background worker đều phụ thuộc vào nó để graceful shutdown và quản lý timeout.

Quan trọng cho Phỏng vấn

Context là một trong những chủ đề được hỏi thường xuyên nhất trong các buổi phỏng vấn Go. Nhà tuyển dụng mong đợi ứng viên giải thích được context propagation, trình bày xử lý hủy bỏ đúng cách và tránh các lỗi phổ biến như lưu context trong struct.

Interface Context và Bốn Phương thức của Nó

Interface Context định nghĩa bốn phương thức mà mọi implementation của context phải thỏa mãn. Hiểu các phương thức này tạo nền tảng cho việc sử dụng context hiệu quả.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() trả về một channel chỉ nhận. Khi channel này đóng, mọi goroutine đang lắng nghe nó nhận được tín hiệu ngay lập tức. Pattern <-ctx.Done() block cho đến khi hủy bỏ xảy ra. Err() giải thích lý do: hoặc là context.Canceled khi bị hủy bỏ rõ ràng, hoặc context.DeadlineExceeded khi timeout hoặc deadline đã qua.

Tạo Context với Background và TODO

Hai hàm tạo root context: context.Background()context.TODO(). Cả hai đều trả về context non-nil, rỗng và không bao giờ bị hủy.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() đóng vai trò là parent cho các incoming request, hàm main và code khởi tạo. TODO() hoạt động như placeholder trong quá trình refactoring khi nguồn context đúng chưa được xác định. Các công cụ phân tích tĩnh có thể đánh dấu việc sử dụng TODO() để review sau.

Hủy bỏ với WithCancel và WithCancelCause

Hủy bỏ thủ công cho phép goroutine cha báo hiệu cho các goroutine con rằng công việc nên dừng. Pattern này xuất hiện trong worker pool, background task và các implementation graceful shutdown.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, được giới thiệu trong Go 1.20, cung cấp thông tin error phong phú hơn so với WithCancel thông thường. Hàm context.Cause() lấy error được truyền vào hàm cancel, giúp debug dễ dàng hơn trong các hệ thống phức tạp.

Sẵn sàng chinh phục phỏng vấn Go?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Timeout và Deadline cho Xử lý Request

HTTP handler, gọi database và request API bên ngoài luôn nên có timeout. Package context cung cấp WithTimeoutWithDeadline cho mục đích này. Cả hai đều tạo context tự động hủy khi hết thời gian.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

Pattern defer cancel() đảm bảo resource được giải phóng ngay cả khi operation hoàn thành trước timeout. Bỏ qua việc gọi này gây ra resource leak: goroutine timer của context vẫn hoạt động cho đến khi timeout hết hạn.

Request-Scoped Values với WithValue

WithValue đính kèm dữ liệu request-scoped vào context. Các use case phổ biến bao gồm request ID, token xác thực và tracing span. Tài liệu chính thức nhấn mạnh việc sử dụng kiểu tùy chỉnh làm key để tránh xung đột.

context_values.gogo
package main

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

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Context value tạo thành chuỗi bất biến: mỗi lần gọi WithValue tạo một context mới bao bọc parent. Việc tìm kiếm duyệt qua chuỗi này, khiến value lồng sâu chậm hơn khi truy cập. Chỉ lưu dữ liệu request-scoped, không phải cấu hình ứng dụng hoặc tham số tùy chọn.

Tiện ích WithoutCancel và AfterFunc

Go 1.21 thêm WithoutCancel cho các operation phải hoàn thành bất kể việc hủy parent, như cleanup task. AfterFunc lên lịch callback khi context bị hủy.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel bảo toàn value từ context parent nhưng bỏ qua tín hiệu hủy của nó. Pattern này phù hợp cho logging, phát metric và database commit phải thành công bất kể kết quả request.

Câu hỏi và Trả lời Phỏng vấn

Chuẩn bị cho phỏng vấn kỹ thuật Go đòi hỏi hiểu về nội bộ context và best practice. Những câu hỏi này thường xuất hiện trong phỏng vấn tại các công ty sử dụng Go cho dịch vụ backend. Để chuẩn bị phỏng vấn Go thêm, xem module câu hỏi phỏng vấn Context Package.

Tại sao context nên là tham số đầu tiên?

Đội ngũ Go thiết lập quy ước này để làm cho context propagation rõ ràng và nhất quán. Đặt context ở vị trí đầu tiên báo hiệu cho người đọc rằng hàm tôn trọng việc hủy. Thư viện tiêu chuẩn tuân theo pattern này: http.Request.Context(), database/sql.QueryContext()grpc.UnaryInterceptor đều mong đợi context là đối số đầu tiên.

Điều gì xảy ra nếu lưu context trong struct?

Lưu context vi phạm mô hình lifecycle request. Một struct có thể tồn tại lâu hơn request tạo ra nó, khiến các operation sử dụng tín hiệu hủy đã cũ hoặc bỏ lỡ deadline mới. Tài liệu context rõ ràng cảnh báo chống lại pattern này.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

Làm thế nào để xử lý context trong goroutine?

Goroutine phải kiểm tra ctx.Done() trong vòng lặp chính hoặc câu lệnh select của chúng. Bỏ qua channel done tạo ra rò rỉ goroutine: hàm parent trả về, nhưng goroutine tiếp tục tiêu thụ resource.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

Khi nào nên sử dụng context.TODO()?

Sử dụng TODO() trong quá trình refactoring từng bước khi thêm hỗ trợ context vào code legacy. Nó đánh dấu các vị trí cần context propagation đúng cách. Code production cuối cùng nên thay thế tất cả các lần gọi TODO() bằng context thực từ request handler hoặc khởi tạo ứng dụng.

Các Lỗi Phổ biến Cần Tránh

Việc sử dụng sai context dẫn đến bug tinh vi xuất hiện khi tải cao hoặc trong quá trình shutdown. Nhận biết các pattern này ngăn ngừa sự cố production.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Điểm Chính khi Sử dụng Go Context trong Production

  • Truyền context làm tham số đầu tiên cho hàm, không bao giờ lưu nó trong struct
  • Luôn defer cancel() sau WithTimeout, WithDeadline hoặc WithCancel để ngăn resource leak
  • Sử dụng WithCancelCausecontext.Cause() để debug error tốt hơn trong chuỗi hủy phức tạp
  • Kiểm tra <-ctx.Done() trong vòng lặp goroutine để cho phép graceful shutdown và ngăn rò rỉ
  • Sử dụng kiểu key tùy chỉnh cho WithValue để tránh xung đột giữa các package
  • Áp dụng WithoutCancel cho các operation cleanup phải hoàn thành bất kể việc hủy parent
  • Ưu tiên context.Background() cho khởi tạo ứng dụng và context.TODO() chỉ trong quá trình refactoring
Thử thách hôm nay

Bạn có tìm ra lỗi trong Go không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 29 tháng 8, 2026

Chia sẻ

Bài viết liên quan