context包在Go并发编程中的作用
Go语言的goroutine轻量级并发模型在微服务架构中广泛使用,但goroutine的取消和超时控制长期缺乏标准方案。context包(context)于Go 1.7引入标准库,提供跨goroutine的取消信号传播、超时控制和请求范围键值传递三种能力。在HTTP服务、gRPC调用和数据库操作中,context贯穿整个请求链路,是Go后端开发的核心基础设施。
context的设计理念是:每个请求创建一个root context,沿着调用链向下传递,任何环节可以派生子context并设置超时或取消条件。当父context取消时,所有子context自动级联取消,触发链路上所有阻塞操作立即返回。这种机制避免了goroutine泄漏问题——当上游请求超时或客户端断开连接时,下游所有正在执行的goroutine能及时退出。
四种context的创建与使用
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. context.Background(): root context,永不取消
ctx := context.Background()
// 2. context.TODO(): 占位context,尚未确定使用哪种context
_ = context.TODO()
// 3. WithCancel: 手动取消
cancelCtx, cancel := context.WithCancel(ctx)
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
result := doWork(cancelCtx)
fmt.Println(result)
// 4. WithTimeout: 超时自动取消
timeoutCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
result2 := doWork(timeoutCtx)
fmt.Println(result2)
// 5. WithDeadline: 截止时间取消
deadline := time.Now().Add(5 * time.Second)
deadlineCtx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
result3 := doWork(deadlineCtx)
fmt.Println(result3)
// 6. WithValue: 携带键值对
valCtx := context.WithValue(ctx, "requestID", "req-12345")
reqID := valCtx.Value("requestID")
fmt.Println(reqID)
}
func doWork(ctx context.Context) string {
select {
case <-ctx.Done():
return fmt.Sprintf("cancelled: %v", ctx.Err())
case <-time.After(3 * time.Second):
return "completed"
}
}
WithCancel、WithTimeout、WithDeadline返回的cancel函数必须被调用,否则context资源(timer、goroutine)不会被释放。使用defer cancel()确保函数退出时释放资源,即使操作正常完成也不例外。这是Go代码审查中最常见的context使用问题。
HTTP请求链路中的context传递
Go标准库的net/http从Go 1.7开始支持context。r.Context()返回的context在客户端断开连接或服务端WriteHeader后自动取消。将此context传递给下游调用,实现整条链路的取消传播。
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 添加请求级数据
ctx = context.WithValue(ctx, "traceID", r.Header.Get("X-Trace-ID"))
// 设置整个请求的超时
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 并行调用多个下游服务
type result struct {
data interface{}
err error
}
ch := make(chan result, 2)
go func() {
data, err := callUserService(ctx)
ch <- result{data, err}
}()
go func() {
data, err := callOrderService(ctx)
ch <- result{data, err}
}()
// 等待所有结果或超时
var userResult, orderResult result
for i := 0; i < 2; i++ {
select {
case res := <-ch:
if i == 0 {
userResult = res
} else {
orderResult = res
}
case <-ctx.Done():
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
}
}
if userResult.err != nil || orderResult.err != nil {
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"user": userResult.data,
"order": orderResult.data,
})
}
func callUserService(ctx context.Context) (*User, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", "http://user-service/api/user", nil)
if traceID, ok := ctx.Value("traceID").(string); ok {
req.Header.Set("X-Trace-ID", traceID)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("context cancelled: %w", ctx.Err())
}
return nil, err
}
defer resp.Body.Close()
var user User
json.NewDecoder(resp.Body).Decode(&user)
return &user, nil
}
http.NewRequestWithContext将context绑定到HTTP请求。当context取消时,底层TCP连接的读写操作立即中断,http.DefaultClient.Do返回错误,避免goroutine在已取消的请求上浪费资源。传递traceID到下游服务实现全链路追踪。
数据库操作中的context超时控制
Go的database/sql包从Go 1.8开始支持context。在查询时传入context,超时后数据库驱动自动取消正在执行的查询:
func getUserByID(ctx context.Context, db *sql.DB, id int64) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?",
id,
).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("query timeout: %w", err)
}
return nil, err
}
return &user, nil
}
// 批量查询使用context取消
func batchGetUsers(ctx context.Context, db *sql.DB, ids []int64) ([]User, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
users := make([]User, len(ids))
errCh := make(chan error, len(ids))
for i, id := range ids {
go func(idx int, uid int64) {
user, err := getUserByID(ctx, db, uid)
if err != nil {
cancel()
errCh <- err
return
}
users[idx] = *user
errCh <- nil
}(i, id)
}
for range ids {
if err := <-errCh; err != nil {
return nil, err
}
}
return users, nil
}
数据库连接池中context超时后,查询被取消但连接会归还到连接池。部分数据库(如MySQL)的驱动在context取消后需要发送KILL QUERY命令终止服务端查询,确保不会在数据库服务器上留下僵尸查询。
WithValue的工程化使用规范
context.WithValue用于在调用链中传递请求范围数据(如traceID、用户身份、租户ID等)。但WithValue的键是interface{}类型,直接使用字符串作为键存在命名冲突风险。Go官方推荐使用自定义类型作为键:
// 定义context键类型
type contextKey string
const (
traceIDKey contextKey = "traceID"
userIDKey contextKey = "userID"
tenantIDKey contextKey = "tenantID"
authInfoKey contextKey = "authInfo"
)
// 类型安全的存取函数
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, traceIDKey, traceID)
}
func TraceIDFromContext(ctx context.Context) string {
if v, ok := ctx.Value(traceIDKey).(string); ok {
return v
}
return ""
}
func WithAuthInfo(ctx context.Context, auth *AuthInfo) context.Context {
return context.WithValue(ctx, authInfoKey, auth)
}
func AuthInfoFromContext(ctx context.Context) *AuthInfo {
if v, ok := ctx.Value(authInfoKey).(*AuthInfo); ok {
return v
}
return nil
}
// 中间件注入
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
auth, err := parseToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := r.Context()
ctx = WithAuthInfo(ctx, auth)
ctx = WithTraceID(ctx, generateTraceID())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
WithValue只用于传递请求范围数据,不用于函数可选参数传递。Go团队明确建议不要通过context传递本应作为函数参数的数据。WithValue的数据应该是不可变的(immutable),避免在子goroutine中并发修改导致数据竞争。使用自定义类型作为键避免命名冲突,封装存取函数确保类型安全,这是生产级Go代码的标准实践。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-bao-shi-zhan-goroutine-chao-shi-kong-zhi/