Go语言的context包是并发编程中传递截止时间、取消信号和请求级值的标准机制。在微服务架构中,一次外部请求可能触发多层内部RPC调用和数据库查询,context确保任何一个环节超时或取消时,整个调用链上的goroutine都能及时退出,避免资源泄漏。正确使用context是Go后端工程的基础能力。
context核心接口与四种创建方式
context.Context是一个接口,定义了四个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
四种创建函数对应不同场景:
// 1. 根context(顶层请求创建)
ctx := context.Background()
ctx := context.TODO()
// 2. 带截止时间
ctx, cancel := context.WithDeadline(parent, time.Now().Add(30*time.Second))
ctx, cancel := context.WithTimeout(parent, 30*time.Second)
defer cancel()
// 3. 带取消信号
ctx, cancel := context.WithCancel(parent)
defer cancel()
// 4. 带值传递
ctx = context.WithValue(parent, "userID", 12345)
userID := ctx.Value("userID")
所有context都从parent派生,形成树状结构。parent取消时所有子context自动取消。
context传播规则与函数签名约定
Go社区约定context作为函数第一个参数,类型为context.Context,名称为ctx:
func FetchUser(ctx context.Context, userID int) (*User, error) {
// 检查context是否已取消
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// 执行数据库查询
row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", userID)
...
}
func HandleRequest(ctx context.Context, req *Request) (*Response, error) {
user, err := FetchUser(ctx, req.UserID)
if err != nil {
return nil, err
}
orders, err := FetchOrders(ctx, user.ID)
...
}
关键原则:
1. 不要将context存储在struct字段中,始终通过函数参数传递
2. 不要传递nil context,使用context.Background()或context.TODO()
3. 不要在无关函数间复用context,每个请求创建独立的context树
4. 收到context后先检查Done channel
超时控制的传播与级联取消
实际场景中,HTTP请求有整体超时,内部各步骤有独立超时。子context的超时不能超过父context:
func main() {
// HTTP请求总超时10秒
rootCtx, rootCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer rootCancel()
// 数据库查询最多3秒
dbCtx, dbCancel := context.WithTimeout(rootCtx, 3*time.Second)
defer dbCancel()
user, err := db.GetUser(dbCtx, 1)
// 如果dbCtx超时(3秒),rootCtx仍然有效
// 如果rootCtx超时(10秒),dbCtx也自动取消
// RPC调用最多5秒
rpcCtx, rpcCancel := context.WithTimeout(rootCtx, 5*time.Second)
defer rpcCancel()
result, err := rpcClient.Call(rpcCtx, "Method", args)
}
超时触发的传播路径:子context超时只影响自身及更深层级,不影响父context。父context超时会级联取消所有子context。
HTTP服务端context集成
net/http从Go 1.7开始自动为每个请求创建context,通过r.Context()获取:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 客户端断开连接时,ctx自动取消
select {
case <-ctx.Done():
log.Println("client disconnected:", ctx.Err())
return
default:
}
// 添加请求级超时
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := processRequest(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func processRequest(ctx context.Context) (Result, error) {
ch := make(chan Result, 1)
errCh := make(chan error, 1)
go func() {
result, err := doWork(ctx)
if err != nil {
errCh <- err
return
}
ch <- result
}()
select {
case <-ctx.Done():
return Result{}, ctx.Err()
case err := <-errCh:
return Result{}, err
case result := <-ch:
return result, nil
}
}
注意goroutine泄漏问题:当ctx超时函数返回后,doWork中的goroutine仍在运行。正确做法是在doWork内部也监听ctx.Done()。
数据库与Redis操作中的context超时
database/sql的Context方法会在context取消时中断底层查询:
func getUser(ctx context.Context, db *sql.DB, id int) (*User, error) {
row := db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = ?", id)
var u User
err := row.Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("query timeout: %w", err)
}
return nil, err
}
return &u, nil
}
// 事务中的context
func transferFunds(ctx context.Context, db *sql.DB, from, to, amount int) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return tx.Commit()
}
Redis操作使用go-redis库,所有命令都支持context:
func cacheGet(ctx context.Context, rdb *redis.Client, key string) (string, error) {
val, err := rdb.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return "", nil
}
if errors.Is(err, context.DeadlineExceeded) {
return "", fmt.Errorf("redis timeout: %w", err)
}
return "", err
}
return val, nil
}
// Pipeline中的context
func batchSet(ctx context.Context, rdb *redis.Client, items map[string]string) error {
pipe := rdb.Pipeline()
for k, v := range items {
pipe.Set(ctx, k, v, time.Hour)
}
cmders, err := pipe.Exec(ctx)
if err != nil {
return err
}
for _, cmd := range cmders {
if cmd.Err() != nil {
log.Printf("set failed: %v", cmd.Err())
}
}
return nil
}
gRPC中的context传播
gRPC框架在客户端和服务端自动处理context传播。客户端设置的deadline通过gRPC metadata传递到服务端:
// 客户端
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
status, ok := status.FromError(err)
if ok && status.Code() == codes.DeadlineExceeded {
log.Println("gRPC call timed out")
}
}
// 服务端
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
deadline, ok := ctx.Deadline()
if !ok {
return nil, status.Error(codes.FailedPrecondition, "no deadline set")
}
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Error(codes.DeadlineExceeded, "insufficient time")
}
user, err := s.repo.Get(ctx, req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "query failed: %v", err)
}
return user, nil
}
context.Value的使用边界与请求级元数据传递
context.Value用于在调用链中传递请求级元数据(如traceID、userID),但不应替代函数参数传递业务数据:
// 定义自定义key类型避免冲突
type contextKey string
const (
userIDKey contextKey = "userID"
traceIDKey contextKey = "traceID"
authInfoKey contextKey = "auth"
)
// 中间件注入元数据
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := verifyToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
traceID := generateTraceID()
ctx := r.Context()
ctx = context.WithValue(ctx, userIDKey, user.ID)
ctx = context.WithValue(ctx, traceIDKey, traceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 在handler中提取
func getUserID(ctx context.Context) (int, bool) {
v, ok := ctx.Value(userIDKey).(int)
return v, ok
}
func getTraceID(ctx context.Context) string {
v, _ := ctx.Value(traceIDKey).(string)
return v
}
使用自定义key类型(而非string)防止不同包之间的key冲突。context.Value的查找是线性扫描,不适合高频读取或大数据量场景。
goroutine泄漏检测与预防
context使用不当最常见的后果是goroutine泄漏。典型模式:
// 泄漏:启动goroutine但忽略ctx取消
func leakyFetch(ctx context.Context) (Result, error) {
ch := make(chan Result, 1)
go func() {
result := slowOperation() // 不检查ctx
ch <- result
}()
select {
case <-ctx.Done():
return Result{}, ctx.Err()
case r := <-ch:
return r, nil
}
}
// 正确:goroutine内部监听ctx
func safeFetch(ctx context.Context) (Result, error) {
ch := make(chan Result, 1)
go func() {
result, err := slowOperation(ctx) // 传递ctx
if err != nil {
return
}
ch <- result
}()
select {
case <-ctx.Done():
return Result{}, ctx.Err()
case r := <-ch:
return r, nil
}
}
使用go.uber.org/goleak在测试中检测泄漏:
import "go.uber.org/goleak"
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
测试结束时若有未退出的goroutine,goleak会报告具体的创建堆栈,帮助定位泄漏点。
生产环境context最佳实践清单
1. 每个HTTP handler创建独立context树,不要复用
2. WithTimeout/WithCancel的返回cancel函数必须调用,使用defer cancel()模式
3. 设置合理的超时层级:HTTP请求5-10s → RPC 3-5s → DB 2-3s → Redis 1s
4. context.Value仅传递请求级元数据,不传递业务参数
5. goroutine内务必监听ctx.Done(),防止泄漏
6. 日志中输出traceID,与context.Value关联实现全链路追踪
7. 对外部依赖(DB、Redis、HTTP)统一使用Context版本的方法
8. 定期使用pprof和goleak检测goroutine数量异常增长
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-shang-xia-wen-chuan-bo-ji-zhi-yu-bing-fa/