Go Context là gì? Hướng dẫn sử dụng Context trong Go
1. Context là gì?
Trong Go, context là một package thuộc thư viện chuẩn, cung cấp cơ chế để quản lý thời hạn thực hiện công việc, gửi tín hiệu hủy tác vụ và truyền một số dữ liệu gắn với quá trình xử lý qua nhiều lời gọi hàm.
Context thường được sử dụng trong các chương trình có nhiều goroutine, các ứng dụng HTTP API và những hệ thống cần phối hợp nhiều tác vụ với nhau.
Ví dụ, khi một API nhận request để lấy thông tin sản phẩm, quá trình xử lý có thể bao gồm nhiều bước:
- Nhận request từ client.
- Gọi service để xử lý nghiệp vụ.
- Truy vấn dữ liệu từ database.
- Gọi một dịch vụ bên ngoài.
- Trả kết quả về cho client.
Nếu client ngắt kết nối hoặc request vượt quá thời gian cho phép, những công việc phía sau có thể không còn cần thiết. Context giúp truyền tín hiệu để các thành phần liên quan biết rằng nên dừng xử lý.
Có thể hiểu đơn giản, Context là cơ chế truyền thông tin về vòng đời của một tác vụ qua nhiều lớp xử lý trong chương trình Go.
Context không trực tiếp thực hiện công việc và cũng không tự động dừng mọi goroutine. Nó cung cấp tín hiệu để các hàm và goroutine chủ động kiểm tra rồi quyết định dừng xử lý.
2. Tại sao cần Context?
Để hiểu vai trò của Context, hãy xét một chương trình thực hiện công việc mất nhiều thời gian.
Giả sử một API nhận request để tạo báo cáo. Server cần truy vấn database và tổng hợp dữ liệu trước khi trả kết quả.
Trong quá trình đó, có thể xảy ra một số tình huống:
- Tác vụ xử lý quá lâu: truy vấn database hoặc gọi dịch vụ bên ngoài mất nhiều thời gian hơn dự kiến.
- Client ngắt kết nối: người dùng đóng trang hoặc ứng dụng không còn chờ kết quả.
- Hết thời gian cho phép: hệ thống đặt giới hạn thời gian cho một request nhưng tác vụ chưa hoàn thành.
- Tác vụ không còn cần thiết: một bước xử lý thất bại khiến những bước tiếp theo không còn ý nghĩa.
Nếu chương trình không có cơ chế thông báo và phối hợp phù hợp, công việc có thể tiếp tục chạy dù kết quả không còn được sử dụng. Điều này gây lãng phí tài nguyên và làm tăng tải cho hệ thống.
Context giúp giải quyết vấn đề bằng cách truyền tín hiệu hủy đến những thành phần liên quan.
Ví dụ, khi một request hết thời hạn, Context có thể phát tín hiệu để service dừng xử lý hoặc để truy vấn database kết thúc sớm nếu thao tác đó hỗ trợ Context.
Cần lưu ý rằng Context không bảo đảm mọi tác vụ sẽ dừng ngay lập tức. Hiệu quả phụ thuộc vào việc các hàm có kiểm tra tín hiệu hủy hay sử dụng những API hỗ trợ Context hay không.
3. Các thành phần cơ bản của Context
Để sử dụng Context, trước tiên cần import package:
import "context"
Package này cung cấp interface context.Context cùng các hàm tạo Context với những cơ chế quản lý khác nhau.
3.1. context.Background() và context.TODO()
Đây là hai hàm thường được sử dụng để tạo Context gốc.
context.Background()
Dùng khi cần một Context gốc và chưa có Context nào khác để kế thừa. Context này không có deadline, không bị hủy và không chứa giá trị nào.
Ví dụ:
ctx := context.Background()
context.TODO()
Dùng khi chưa xác định được Context nào phù hợp hoặc trong quá trình phát triển, khi cần bổ sung Context sau.
ctx := context.TODO()
Hai hàm này đều trả về Context không có deadline, không có tín hiệu hủy và không có dữ liệu gắn kèm. Khác biệt chính nằm ở ý nghĩa sử dụng: Background() thể hiện một điểm bắt đầu phù hợp, còn TODO() thể hiện rằng lựa chọn Context vẫn cần được xác định.
3.2. context.WithCancel()
context.WithCancel() tạo một Context con có thể được hủy chủ động thông qua hàm cancel.
Cú pháp:
ctx, cancel := context.WithCancel(parent)
Trong đó:
parentlà Context cha.ctxlà Context con được tạo ra.cancellà hàm dùng để gửi tín hiệu hủy Context con.
Ví dụ:
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println(ctx.Err())
}
Kết quả:
context canceled
Trong ví dụ này, gọi cancel() khiến Context bị hủy. Khi đó, ctx.Err() trả về context.Canceled.
Thông thường, nên gọi defer cancel() ngay sau khi tạo Context nếu hàm hiện tại chịu trách nhiệm quản lý vòng đời của nó. Việc này giúp giải phóng các tài nguyên liên quan khi không còn cần Context nữa.
3.3. context.WithTimeout()
context.WithTimeout() tạo một Context có thời gian tồn tại tối đa được tính từ thời điểm tạo.
Cú pháp:
ctx, cancel := context.WithTimeout(parent, duration)
Ví dụ:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
<-ctx.Done()
fmt.Println(ctx.Err())
}
Sau khoảng hai giây, Context hết thời hạn và ctx.Done() được đóng. Khi đó, ctx.Err() trả về context.DeadlineExceeded.
WithTimeout() phù hợp khi muốn giới hạn thời gian xử lý, chẳng hạn chỉ cho phép một request chờ truy vấn database tối đa hai giây.
Lưu ý rằng việc Context hết thời hạn không tự động chấm dứt mọi công việc đang chạy. Các hàm thực hiện công việc cần phối hợp với Context để dừng khi phù hợp.
3.4. context.WithDeadline()
context.WithDeadline() cũng tạo một Context có giới hạn thời gian, nhưng thay vì chỉ định khoảng thời gian tính từ hiện tại, hàm này nhận một thời điểm cụ thể.
Cú pháp:
ctx, cancel := context.WithDeadline(parent, deadline)
Ví dụ:
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(
context.Background(),
deadline,
)
defer cancel()
Trong ví dụ trên, Context hết hạn tại thời điểm được xác định bởi deadline, xấp xỉ ba giây sau khi tạo.
Sự khác biệt chính giữa hai hàm:
WithTimeout()nhận một khoảng thời gian, chẳng hạn3 * time.Second.WithDeadline()nhận một thời điểm kết thúc cụ thể, chẳng hạn một giá trịtime.Time.
Cả hai đều trả về một Context con và hàm cancel.
3.5. ctx.Done() và ctx.Err()
Đây là hai thành phần quan trọng để kiểm tra trạng thái của Context.
ctx.Done()
Trả về một channel chỉ đọc. Channel này được đóng khi Context bị hủy hoặc hết thời hạn.
<-ctx.Done()
Câu lệnh trên sẽ chờ cho đến khi Context bị hủy hoặc hết thời hạn.
ctx.Err()
Trả về nguyên nhân Context đã kết thúc:
context.Canceled: Context bị hủy chủ động.context.DeadlineExceeded: Context đã hết thời hạn.
Nếu Context chưa bị hủy và chưa hết hạn, ctx.Err() trả về nil.
Có thể kết hợp hai thành phần này để chương trình chờ tín hiệu hủy hoặc tiếp tục thực hiện công việc khác, thay vì chờ vô thời hạn.
4. Context phối hợp với Goroutine như thế nào?
Trong Go, goroutine là đơn vị thực thi nhẹ, cho phép chương trình thực hiện nhiều công việc đồng thời. Tuy nhiên, khi một goroutine không còn cần thiết, chương trình vẫn phải có cơ chế để thông báo và phối hợp việc dừng nó.
Context có thể đảm nhiệm vai trò gửi tín hiệu hủy đó.
Ví dụ:
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Work stopped:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
go doWork(ctx)
<-ctx.Done()
fmt.Println("Main finished:", ctx.Err())
}
Cách hoạt động
main()tạo Context có thời hạn hai giây.go doWork(ctx)khởi chạy công việc trong một goroutine.- Trong vòng lặp,
doWork()sử dụngselectđể kiểm tra tín hiệu từctx.Done(). - Khi Context hết thời hạn, channel
ctx.Done()được đóng. - Goroutine nhận biết tín hiệu hủy, in thông báo và kết thúc hàm.
Trong ví dụ này, main() chờ Context kết thúc. Tuy nhiên, việc Context kết thúc không đồng nghĩa với việc goroutine chắc chắn đã hoàn thành toàn bộ quá trình dọn dẹp. Nếu cần xác nhận goroutine đã thực sự kết thúc, chương trình nên bổ sung cơ chế đồng bộ như sync.WaitGroup hoặc một channel thông báo hoàn thành.
Một điểm cần hiểu rõ là Context chỉ cung cấp tín hiệu hủy; goroutine phải chủ động kiểm tra và phản hồi tín hiệu đó. Nếu goroutine thực hiện một thao tác chặn không hỗ trợ Context và không kiểm tra tín hiệu hủy, nó vẫn có thể tiếp tục chạy.
5. Ứng dụng Context trong HTTP và API
Context đặc biệt hữu ích trong các ứng dụng HTTP API vì mỗi request thường kéo theo nhiều thao tác xử lý liên quan.
5.1. Giới hạn thời gian xử lý
Giả sử một API cần gọi dịch vụ bên ngoài để lấy dữ liệu. Nếu dịch vụ phản hồi quá chậm, request có thể chiếm tài nguyên trong thời gian dài.
Có thể tạo Context với timeout:
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
Sau đó, truyền ctx đến hàm xử lý hoặc API hỗ trợ Context.
Khi hết thời hạn, các thao tác có hỗ trợ Context có thể dừng sớm và trả về lỗi timeout.
5.2. Khi client ngắt kết nối
Trong ứng dụng HTTP server của Go, mỗi request có Context riêng, có thể lấy bằng:
ctx := r.Context()
Trong đó, r là *http.Request.
Context này có thể bị hủy khi client kết nối bị đóng hoặc request bị hủy, tùy theo cách kết nối và server xử lý.
Ví dụ:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := fetchData(ctx)
if err != nil {
http.Error(
w,
"Request failed",
http.StatusInternalServerError,
)
return
}
fmt.Fprintln(w, result)
}
Trong ví dụ trên, fetchData() nhận Context để có thể phối hợp với vòng đời của request. Đây là hàm minh họa; khi triển khai thực tế, cần định nghĩa hàm này và lựa chọn mã trạng thái phù hợp với từng loại lỗi.
Nếu fetchData() gọi database hoặc một dịch vụ hỗ trợ Context, nó có thể truyền tiếp ctx để những thao tác đó phản hồi tín hiệu hủy.
5.3. Sử dụng Context với database
Khi truy vấn database, nên sử dụng những API có hỗ trợ Context nếu cần giới hạn thời gian hoặc phối hợp việc hủy tác vụ.
Ví dụ với database/sql:
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
rows, err := db.QueryContext(
ctx,
"SELECT id, name FROM products",
)
if err != nil {
// Xử lý lỗi truy vấn.
return
}
defer rows.Close()
Trong ví dụ này, truy vấn được thực hiện thông qua QueryContext(). Nếu Context hết thời hạn hoặc bị hủy, driver và database có khả năng hỗ trợ hủy sẽ phối hợp để dừng truy vấn.
Khả năng dừng và thời điểm truy vấn thực sự kết thúc còn phụ thuộc vào driver, database và thao tác đang thực hiện.
5.4. Context trong Gin-Gonic
Gin là một framework HTTP phổ biến trong Go. Trong Gin handler, cần phân biệt *gin.Context với context.Context của thư viện chuẩn.
*gin.Contextcung cấp các chức năng của Gin để xử lý request, response, tham số và middleware.context.Contextcung cấp cơ chế quản lý deadline, hủy tác vụ và dữ liệu gắn với vòng đời xử lý.
Có thể lấy Context chuẩn từ HTTP request của Gin:
func getProducts(c *gin.Context) {
ctx := c.Request.Context()
products, err := productService.GetAll(ctx)
if err != nil {
c.JSON(500, gin.H{
"error": "Unable to get products",
})
return
}
c.JSON(200, products)
}
Ví dụ trên giả định đã có productService.GetAll(ctx) và kiểu dữ liệu products phù hợp. Trong dự án thực tế, nên sử dụng mã trạng thái cụ thể cho từng loại lỗi thay vì trả 500 cho tất cả trường hợp.
Truyền Context từ handler xuống service và database giúp các tầng xử lý phối hợp với nhau trong suốt vòng đời request.
6. Những lưu ý khi sử dụng Context
6.1. Context không tự động dừng goroutine
Việc gọi cancel() hoặc để Context hết thời hạn chỉ phát tín hiệu hủy. Các goroutine phải kiểm tra ctx.Done() hoặc sử dụng API hỗ trợ Context để phản hồi tín hiệu đó.
6.2. Không truyền nil làm Context
Khi một hàm cần Context, thông thường nên truyền một Context hợp lệ như context.Background(), context.TODO() hoặc Context nhận từ request. Không nên truyền nil, vì nhiều hàm có thể cần gọi phương thức trên Context và sẽ gặp lỗi khi nhận giá trị nil.
6.3. Gọi cancel() khi phù hợp
Các hàm như WithCancel(), WithTimeout() và WithDeadline() trả về một hàm cancel. Nên gọi hàm này khi không còn cần Context để giải phóng những tài nguyên liên quan.
Cách thường gặp là:
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
6.4. Không dùng Context để truyền tham số nghiệp vụ thông thường
Context không nên được sử dụng như một túi chứa mọi dữ liệu của ứng dụng. Các tham số nghiệp vụ như productID, price hoặc userInput nên được truyền qua tham số hàm rõ ràng.
Giá trị trong Context phù hợp hơn với dữ liệu gắn với request và cần đi qua nhiều tầng xử lý, chẳng hạn mã định danh request hoặc thông tin phục vụ truy vết. Việc sử dụng context.WithValue() nên có chủ đích, tránh lạm dụng.
6.5. Truyền Context qua các tầng xử lý
Trong một ứng dụng thực tế, Context thường được truyền từ tầng tiếp nhận request xuống các tầng service, repository và database.
Ví dụ:
HTTP Handler → Service → Repository → Database
Cách tổ chức này giúp các tầng phối hợp việc hủy tác vụ và giới hạn thời gian mà không cần tạo một Context hoàn toàn độc lập ở mỗi tầng.
Kết luận
Context là một cơ chế quan trọng trong Go để quản lý vòng đời của các tác vụ, đặc biệt trong những chương trình sử dụng goroutine và các ứng dụng HTTP API.
Các hàm như WithCancel(), WithTimeout() và WithDeadline() cho phép tạo Context với những điều kiện kết thúc khác nhau. Trong khi đó, Done() và Err() giúp chương trình nhận biết và phản hồi tín hiệu hủy.
Điều quan trọng nhất cần nhớ là Context không thực hiện việc hủy thay cho chương trình; nó cung cấp tín hiệu để các thành phần liên quan phối hợp dừng công việc khi không còn cần thiết.
Hiểu và sử dụng Context đúng cách giúp ứng dụng Go quản lý tài nguyên hiệu quả hơn, xử lý timeout rõ ràng hơn và phối hợp các tác vụ trong hệ thống một cách có kiểm soát.








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




