1. Go Context là gì?
Trong ngôn ngữ lập trình Go, Context là một cơ chế được sử dụng để quản lý thời gian thực thi, hủy bỏ các tác vụ và truyền một số thông tin cần thiết trong quá trình xử lý của chương trình.
Context thường được sử dụng khi làm việc với Goroutine, gọi API, truy vấn cơ sở dữ liệu hoặc xử lý các tác vụ có thể mất nhiều thời gian. Ví dụ, khi một chương trình gửi yêu cầu đến máy chủ nhưng phải chờ quá lâu mới nhận được phản hồi, Context có thể giúp giới hạn thời gian chờ hoặc yêu cầu dừng tác vụ khi không còn cần thiết.
Context thuộc package context có sẵn trong thư viện chuẩn của Go. Để sử dụng, chúng ta cần import package này vào chương trình.
Ví dụ:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
fmt.Println(ctx)
}
Trong ví dụ trên, context.Background() tạo ra một Context gốc, thường được sử dụng để bắt đầu một luồng xử lý mới. Đây là điểm khởi đầu để tạo ra những Context khác phục vụ cho các tác vụ cụ thể.

2. Tại sao cần sử dụng Context?
Trong một chương trình Go, nhiều tác vụ có thể chạy đồng thời thông qua Goroutine. Tuy nhiên, nếu không có cơ chế quản lý phù hợp, một số tác vụ có thể tiếp tục chạy dù kết quả của chúng không còn cần thiết.
Ví dụ, người dùng gửi yêu cầu lấy dữ liệu từ máy chủ nhưng sau đó đóng trang hoặc hủy yêu cầu. Nếu chương trình phía máy chủ vẫn tiếp tục truy vấn cơ sở dữ liệu hoặc gọi một dịch vụ khác, tài nguyên hệ thống sẽ bị sử dụng không cần thiết.
Context giúp giải quyết vấn đề này bằng cách cho phép chương trình thông báo rằng một tác vụ cần dừng lại. Nó cũng hỗ trợ giới hạn thời gian thực hiện và truyền các giá trị liên quan đến phạm vi của một yêu cầu.
Một số mục đích chính của Context bao gồm:
- Hủy tác vụ: Thông báo cho Goroutine biết rằng công việc không cần tiếp tục nữa.
- Giới hạn thời gian: Dừng chờ khi một tác vụ vượt quá thời gian cho phép.
- Quản lý tác vụ đồng thời: Truyền tín hiệu hủy từ tác vụ cha đến các tác vụ con.
- Truyền dữ liệu theo yêu cầu: Chuyển một số thông tin như mã yêu cầu (request ID) qua các hàm có liên quan.
Cần lưu ý rằng Context không tự động dừng một Goroutine. Nó chỉ gửi tín hiệu để Goroutine có thể kiểm tra và chủ động kết thúc công việc.

3. Các loại Context thường dùng trong Go
Package context cung cấp một số hàm quan trọng để tạo Context phù hợp với từng tình huống.
3.1. context.Background()
context.Background() tạo ra một Context gốc không có thời hạn và không thể bị hủy trực tiếp. Nó thường được dùng tại điểm bắt đầu của chương trình, trong hàm main hoặc khi cần tạo một Context mới độc lập.
Ví dụ:
ctx := context.Background()
Ở đây, biến ctx lưu Context gốc. Bản thân Context này không tự đặt thời gian chờ và cũng không có tín hiệu hủy. Từ nó, chúng ta có thể tạo ra Context có thời hạn hoặc có khả năng hủy.
3.2. context.TODO()
context.TODO() cũng tạo ra một Context gốc, tương tự context.Background(). Hàm này thường được dùng khi chưa xác định được Context phù hợp hoặc khi đang xây dựng, chỉnh sửa chương trình và sẽ bổ sung cách quản lý Context sau.
Ví dụ:
ctx := context.TODO()
Về chức năng thực tế, Background() và TODO() gần như giống nhau. Điểm khác biệt chủ yếu nằm ở ý nghĩa sử dụng: Background() thể hiện đây là Context gốc được lựa chọn có chủ đích, còn TODO() thể hiện rằng việc lựa chọn Context vẫn cần được hoàn thiện.
3.3. context.WithCancel()
context.WithCancel() tạo ra một Context có thể bị hủy theo yêu cầu. Hàm này trả về hai giá trị: Context mới và một hàm cancel dùng để gửi tín hiệu hủy.
Ví dụ:
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cancel()
fmt.Println(ctx.Err())
}
Trong ví dụ này:
context.WithCancel(context.Background())tạo Context mới có khả năng hủy.ctxlà Context được sử dụng để kiểm tra trạng thái của tác vụ.cancellà hàm gửi tín hiệu hủy.defer cancel()đảm bảo hàm hủy được gọi khimainkết thúc.- Lệnh
cancel()chủ động gửi tín hiệu hủy ngay trong ví dụ. ctx.Err()trả vềcontext.Canceledvì Context đã bị hủy.
Trong chương trình thực tế, hàm cancel thường được gọi khi công việc hoàn thành, người dùng hủy yêu cầu hoặc chương trình không còn cần kết quả của tác vụ đó.
3.4. context.WithTimeout()
context.WithTimeout() tạo Context có giới hạn thời gian. Nếu thời gian quy định đã hết mà tác vụ chưa hoàn thành, Context sẽ tự động phát tín hiệu hủy.
Ví dụ:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task canceled:", ctx.Err())
}
}
Trong ví dụ trên, Context được thiết lập thời gian chờ là 2 giây, trong khi công việc giả lập cần 3 giây mới hoàn thành.
2*time.Secondbiểu thị thời gian chờ tối đa là 2 giây.time.After(3 * time.Second)tạo tín hiệu sau 3 giây.ctx.Done()là một channel sẽ được đóng khi Context bị hủy hoặc hết thời hạn.selectchờ một trong hai sự kiện xảy ra.
Do Context hết thời hạn trước khi công việc hoàn thành, chương trình sẽ đi vào nhánh ctx.Done() và in ra lỗi context deadline exceeded.
Lưu ý: ví dụ này chỉ minh họa cách chờ tín hiệu. Trong chương trình thực tế, khi nhận được tín hiệu hủy, tác vụ phải kiểm tra Context và chủ động dừng lại.
3.5. context.WithDeadline()
context.WithDeadline() cũng tạo Context có giới hạn thời gian, nhưng thay vì chỉ định khoảng thời gian chờ, chúng ta chỉ định một thời điểm cụ thể mà Context phải hết hạn.
Ví dụ:
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(
context.Background(),
deadline,
)
defer cancel()
Trong trường hợp này, thời hạn được xác định bằng thời điểm hiện tại cộng thêm 5 giây.
Sự khác biệt giữa WithTimeout() và WithDeadline() là:
WithTimeout()nhận vào khoảng thời gian chờ, chẳng hạn 5 giây.WithDeadline()nhận vào một thời điểm cụ thể, chẳng hạn 10 giờ 30 phút ngày hôm nay.
Cả hai đều giúp giới hạn thời gian xử lý và đều trả về một hàm cancel cần được gọi để giải phóng tài nguyên khi không còn cần Context.
4. Context hoạt động với Goroutine như thế nào?
Context thường được sử dụng cùng Goroutine để quản lý các tác vụ chạy đồng thời. Một Goroutine có thể theo dõi ctx.Done() để biết khi nào cần dừng.
Ví dụ:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker stopped")
return
default:
fmt.Println("Worker is running")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(500 * time.Millisecond)
}
Giải thích cách hoạt động:
- Hàm
worker()nhận Context làm tham số để có thể theo dõi tín hiệu hủy. - Vòng lặp
forcho phép Goroutine tiếp tục thực hiện công việc. selectkiểm tra xemctx.Done()đã có tín hiệu hay chưa.- Khi chưa bị hủy, chương trình in ra thông báo
Worker is running. - Sau 2 giây, hàm
cancel()được gọi trongmain. - Context gửi tín hiệu hủy, khiến Goroutine đi vào nhánh
ctx.Done(), in thông báo và kết thúc bằngreturn.
Điểm quan trọng là Context không tự buộc Goroutine dừng ngay lập tức. Goroutine cần kiểm tra tín hiệu và xử lý việc kết thúc phù hợp. Nếu một hàm bị mắc kẹt trong một thao tác không kiểm tra Context, việc hủy Context không đảm bảo thao tác đó sẽ dừng ngay.
5. ctx.Done() và ctx.Err() là gì?
Đây là hai thành phần thường được sử dụng khi làm việc với Context.
5.1. ctx.Done()
ctx.Done() trả về một channel nhận tín hiệu khi Context bị hủy hoặc hết thời hạn. Channel này thường được sử dụng trong câu lệnh select để phát hiện khi nào cần kết thúc tác vụ.
Ví dụ:
select {
case <-ctx.Done():
return
}
Khi Context bị hủy hoặc hết hạn, channel Done() được đóng. Lệnh nhận dữ liệu từ channel này sẽ được mở khóa, cho phép chương trình xử lý tín hiệu và kết thúc hàm.
5.2. ctx.Err()
ctx.Err() dùng để kiểm tra nguyên nhân khiến Context không còn hoạt động bình thường do bị hủy hoặc hết hạn.
Hai kết quả phổ biến là:
context.Canceled: Context đã bị hủy theo yêu cầu.context.DeadlineExceeded: Context đã hết thời hạn cho phép.
Ví dụ:
if ctx.Err() != nil {
fmt.Println("Context ended:", ctx.Err())
}
Đoạn code kiểm tra xem Context đã bị hủy hoặc hết hạn hay chưa. Nếu ctx.Err() khác nil, chương trình có thể xử lý trạng thái đó, chẳng hạn dừng công việc hoặc ghi lại thông tin cần thiết.
6. Sử dụng Context khi gọi API
Trong các ứng dụng backend, Context rất hữu ích khi gửi yêu cầu HTTP đến một dịch vụ khác. Nếu dịch vụ phản hồi quá chậm hoặc yêu cầu bị hủy, Context có thể giúp giới hạn thời gian chờ.
Ví dụ:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://example.com",
nil,
)
if err != nil {
fmt.Println("Create request failed:", err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("Request failed:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.StatusCode)
}
Trong ví dụ trên:
context.WithTimeout()tạo Context với thời gian chờ tối đa 3 giây.http.NewRequestWithContext()gắn Context vào yêu cầu HTTP.http.DefaultClient.Do(req)thực hiện yêu cầu đến máy chủ.- Nếu Context bị hủy hoặc hết thời hạn trong quá trình yêu cầu đang được xử lý, thao tác HTTP có thể kết thúc với lỗi.
defer resp.Body.Close()đảm bảo phần nội dung phản hồi được đóng khi hàm kết thúc.
Ví dụ sử dụng example.com để minh họa cách gọi HTTP. Trong ứng dụng thực tế, URL sẽ được thay bằng API mà hệ thống cần truy cập.
Việc sử dụng Context trong các yêu cầu HTTP giúp ứng dụng tránh chờ vô thời hạn và quản lý tài nguyên tốt hơn. Tuy nhiên, thời gian chờ của Context không thay thế hoàn toàn các giới hạn khác, chẳng hạn giới hạn thời gian kết nối hoặc cấu hình timeout riêng của HTTP Client.
7. Sử dụng Context khi truy vấn cơ sở dữ liệu
Context cũng được sử dụng khi làm việc với cơ sở dữ liệu. Một truy vấn có thể mất nhiều thời gian do dữ liệu lớn, hệ thống quá tải hoặc kết nối không ổn định.
Ví dụ:
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
rows, err := db.QueryContext(
ctx,
"SELECT id, name FROM users",
)
if err != nil {
fmt.Println("Query failed:", err)
return
}
defer rows.Close()
Trong đoạn code trên:
- Context được thiết lập thời gian chờ tối đa 5 giây.
db.QueryContext()thực hiện truy vấn dữ liệu và nhận Context làm tham số.- Nếu Context bị hủy hoặc hết hạn, thư viện cơ sở dữ liệu có thể yêu cầu thao tác truy vấn dừng lại.
rows.Close()đảm bảo kết quả truy vấn được đóng sau khi sử dụng.
Cần lưu ý rằng khả năng hủy truy vấn còn phụ thuộc vào driver và hệ quản trị cơ sở dữ liệu. Context giúp truyền tín hiệu hủy, nhưng không phải mọi thao tác đều được đảm bảo dừng ngay lập tức.
8. context.WithValue() là gì?
context.WithValue() được sử dụng để tạo Context mới có chứa một cặp khóa và giá trị. Cách này thường dùng để truyền thông tin gắn với một yêu cầu, chẳng hạn request ID, qua nhiều tầng xử lý mà không cần thêm tham số riêng cho từng hàm.
Ví dụ:
type contextKey string
func main() {
ctx := context.WithValue(
context.Background(),
contextKey("requestID"),
"REQ-001",
)
requestID := ctx.Value(contextKey("requestID"))
fmt.Println(requestID)
}
Trong ví dụ trên:
contextKeylà một kiểu dữ liệu riêng được tạo từstring.context.WithValue()tạo Context mới và lưu giá trị"REQ-001"với khóa"requestID".ctx.Value()lấy lại giá trị dựa trên khóa tương ứng.- Kết quả được in ra là
REQ-001.
Khi sử dụng WithValue(), nên định nghĩa kiểu khóa riêng thay vì dùng chuỗi thông thường để hạn chế xung đột khóa giữa các package.
Ngoài ra, không nên sử dụng Context để truyền mọi loại dữ liệu hoặc thay thế các tham số thông thường của hàm. Nó phù hợp hơn với những thông tin liên quan đến phạm vi của một yêu cầu, chẳng hạn mã theo dõi yêu cầu hoặc thông tin phục vụ quá trình xử lý xuyên suốt các tầng của ứng dụng.
9. Một số lưu ý khi sử dụng Context
Khi làm việc với Context trong Go, cần chú ý một số nguyên tắc sau:
Thứ nhất, truyền Context vào hàm cần thực hiện công việc. Thông thường, Context được truyền làm tham số đầu tiên và đặt tên là ctx. Điều này giúp các hàm dễ dàng nhận biết và sử dụng Context.
Thứ hai, luôn gọi hàm cancel khi cần. Các hàm như WithCancel(), WithTimeout() và WithDeadline() đều trả về một hàm hủy. Sử dụng defer cancel() trong những trường hợp phù hợp giúp giải phóng tài nguyên liên quan sớm hơn khi tác vụ hoàn thành.
Thứ ba, không nên bỏ qua tín hiệu hủy. Nếu một Goroutine cần chạy trong thời gian dài, nên kiểm tra ctx.Done() để có thể kết thúc khi không còn cần thiết.
Thứ tư, không truyền nil làm Context. Nếu cần một Context gốc, hãy sử dụng context.Background() hoặc context.TODO() tùy tình huống.
Thứ năm, không lạm dụng context.WithValue(). Chỉ nên lưu những thông tin thực sự gắn với phạm vi của yêu cầu, thay vì dùng Context như một nơi lưu trữ dữ liệu tổng quát.
Thứ sáu, hiểu rằng hủy Context không đồng nghĩa với dừng mọi công việc ngay lập tức. Các hàm thực hiện công việc phải hỗ trợ Context hoặc chủ động kiểm tra tín hiệu hủy để có thể dừng đúng cách.
10. Kết luận
Go Context là một thành phần quan trọng trong việc quản lý các tác vụ của ứng dụng Go, đặc biệt khi chương trình sử dụng Goroutine, gọi API hoặc truy vấn cơ sở dữ liệu. Context giúp truyền tín hiệu hủy, giới hạn thời gian thực hiện và chuyển một số thông tin liên quan đến yêu cầu qua nhiều tầng xử lý.
Các hàm thường gặp gồm context.Background(), context.TODO(), context.WithCancel(), context.WithTimeout(), context.WithDeadline() và context.WithValue(). Bên cạnh đó, ctx.Done() và ctx.Err() giúp chương trình nhận biết khi Context bị hủy hoặc hết hạn.
Nếu sử dụng đúng cách, Context giúp ứng dụng quản lý tài nguyên hiệu quả hơn, hạn chế những tác vụ không còn cần thiết và xây dựng các hệ thống backend ổn định, dễ kiểm soát hơn.








Khoá học lập trình game con rắn cho trẻ em




