前言
Context(上下文)是Go语言中处理请求作用域、取消信号和超时控制的核心机制。在HTTP服务、数据库操作、RPC调用等场景中,Context无处不在。正确使用Context是编写健壮Go服务的基本功。本文深入剖析Context的四种创建方法和实际工程应用。
一、Context的本质
1.1 Context接口
type Context interface {
Deadline() (deadline time.Time, ok bool) // 获取截止时间
Done() <-chan struct{} // 获取取消信号通道
Err() error // 获取取消原因
Value(key any) any // 获取上下文中存储的值
}
1.2 内置Context类型
// 根Context:Background和TODO是空的Context实现
var (
Background = new(emptyCtx)
TODO = new(emptyCtx)
)
// emptyCtx实现
type emptyCtx int
func (emptyCtx) Deadline() (time.Time, bool) { return time.Time{}, false }
func (emptyCtx) Done() <-chan struct{} { return nil }
func (emptyCtx) Err() error { return nil }
func (emptyCtx) Value(key any) any { return nil }
二、创建Context
2.1 WithCancel - 手动取消
import "context"
func main() {
// 创建一个可取消的Context
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // 触发取消
}()
select {
case <-ctx.Done():
fmt.Printf("取消: %v\n", ctx.Err())
case <-time.After(5 * time.Second):
fmt.Println("5秒后超时")
}
}
2.2 WithTimeout - 超时取消
func main() {
// 创建一个1秒超时的Context
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
doTask(ctx)
}
func doTask(ctx context.Context) {
for i := 0; i < 5; i++ {
select {
case <-ctx.Done():
fmt.Printf("任务取消: %v\n", ctx.Err())
return
default:
fmt.Printf("执行任务 %d\n", i)
time.Sleep(500 * time.Millisecond)
}
}
fmt.Println("任务完成")
}
2.3 WithDeadline - 绝对时间截止
func main() {
// 设置截止时间为3秒后
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
doTask(ctx)
}
2.4 WithValue - 传递请求级数据
type key string
const UserIDKey key = "user_id"
const RequestIDKey key = "request_id"
func main() {
// 创建带值的Context
ctx := context.WithValue(context.Background(), UserIDKey, 12345)
ctx = context.WithValue(ctx, RequestIDKey, "req-001")
// 在函数中获取值
userID := ctx.Value(UserIDKey).(int)
reqID := ctx.Value(RequestIDKey).(string)
fmt.Printf("userID: %d, requestID: %s\n", userID, reqID)
}
三、Context传递链
3.1 图解Context树
Background
│
┌───────────────┼───────────────┐
│ │ │
WithCancel WithTimeout WithValue
│ │ │
▼ ▼ ▼
child1 child2 child3
│ │ │
▼ ▼ ▼
child4 child5 child4
3.2 层级取消
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 启动子任务
go subTask("子任务1", ctx)
go subTask("子任务2", ctx)
time.Sleep(2 * time.Second)
fmt.Println("主任务取消")
cancel() // 取消所有子任务
time.Sleep(1 * time.Second)
}
func subTask(name string, ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: 收到取消信号: %v\n", name, ctx.Err())
return
default:
fmt.Printf("%s: 执行中...\n", name)
time.Sleep(500 * time.Millisecond)
}
}
}
3.3 超时传递
func main() {
// 2秒超时的Context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 启动多个层级的任务
level1(ctx)
}
func level1(ctx context.Context) {
fmt.Println("Level 1 开始")
ctx, cancel := context.WithTimeout(ctx, 1*time.Second) // 继承但缩短超时
defer cancel()
level2(ctx)
fmt.Println("Level 1 结束")
}
func level2(ctx context.Context) {
fmt.Println("Level 2 开始")
time.Sleep(1500 * time.Millisecond) // 模拟长时间操作
fmt.Println("Level 2 结束")
}
四、工程实践
4.1 HTTP服务中的Context
import (
"net/http"
"context"
"fmt"
)
func main() {
http.HandleFunc("/api/data", handleData)
server := &http.Server{
Addr: ":8080",
Handler: nil,
}
// 优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("服务器关闭: %v\n", err)
}
}
func handleData(w http.ResponseWriter, r *http.Request) {
// 从请求中获取Context
ctx := r.Context()
// 检查是否取消
select {
case <-ctx.Done():
fmt.Printf("请求已取消: %v\n", ctx.Err())
http.Error(w, "Request cancelled", 499) // Client Closed Request
return
default:
}
// 处理请求
data := fetchData(ctx)
w.Write([]byte(data))
}
func fetchData(ctx context.Context) string {
select {
case <-ctx.Done():
return ""
case <-time.After(1 * time.Second):
return "data"
}
}
4.2 数据库操作中的Context
import (
"database/sql"
"context"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func queryDB(ctx context.Context, db *sql.DB) ([]string, error) {
// 带超时的查询
rows, err := db.QueryContext(ctx, "SELECT name FROM users LIMIT 10")
if err != nil {
return nil, fmt.Errorf("查询失败: %w", err)
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
return names, rows.Err()
}
// 使用示例
func useDB() {
db, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/db")
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
names, err := queryDB(ctx, db)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("查询超时")
}
}
}
4.3 gRPC中的Context
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
type Server struct{}
func (s *Server) UnaryEcho(ctx context.Context, req *EchoRequest) (*EchoResponse, error) {
// 检查Context是否已取消
select {
case <-ctx.Done():
return nil, status.Errorf(status.Canceled, "请求被取消")
default:
}
// 处理请求
return &EchoResponse{Message: req.Message}, nil
}
// 客户端调用
func clientCall(conn *grpc.ClientConn) {
client := NewEchoClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.UnaryEcho(ctx, &EchoRequest{Message: "hello"})
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("RPC调用超时")
}
}
}
4.4 Context值传递的最佳实践
// 定义key类型(避免字符串key冲突)
package middleware
import "context"
type contextKey string
const (
RequestIDKey contextKey = "request_id"
UserIDKey contextKey = "user_id"
TraceIDKey contextKey = "trace_id"
)
// 添加请求ID中间件
func WithRequestID(ctx context.Context, reqID string) context.Context {
return context.WithValue(ctx, RequestIDKey, reqID)
}
// 获取请求ID
func GetRequestID(ctx context.Context) string {
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
return reqID
}
return ""
}
// HTTP中间件示例
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = generateUUID()
}
// 将reqID放入Context
ctx := WithRequestID(r.Context(), reqID)
w.Header().Set("X-Request-ID", reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
五、Context注意事项
5.1 不要传递nil Context
// 错误
func badFunc(ctx context.Context) { // ctx可能是nil!
// ...
}
// 正确:使用context.Background()作为根
func goodFunc(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
// ...
}
5.2 Context值传递的规范
// 1. 定义包级私有的key类型
type key int
const (
myKey key = iota
)
// 2. 避免冲突的key
// 错误:使用字符串"user_id",可能与其他包冲突
// 正确:定义自己的key类型
// 3. 提供获取函数
func FromContext(ctx context.Context) (*MyData, bool) {
val := ctx.Value(myKey)
if val == nil {
return nil, false
}
return val.(*MyData), true
}
5.3 Context的Done通道
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Done()返回的通道
done := ctx.Done()
fmt.Printf("Done() == nil: %t\n", done == nil) // false
cancel()
// 取消后,Done()通道被关闭
<-done // 阻塞,直到通道关闭
fmt.Println("收到取消信号")
}
六、常见面试题
Q1: Context的使用场景
-
超时控制:数据库查询、HTTP请求、RPC调用
-
取消信号:用户取消操作、依赖服务不可用
-
请求级数据:传递request_id、user_id等
-
优雅关闭:服务停止时取消正在进行的操作
Q2: WithCancel、WithTimeout、WithDeadline区别
// WithCancel:手动取消
ctx, cancel := context.WithCancel(parent)
// WithTimeout:相对时间超时
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
// WithDeadline:绝对时间截止
ctx, cancel := context.WithDeadline(parent, someFutureTime)
Q3: Context的层级关系
// 子Context继承父Context的所有属性
// 取消时,子Context也会被取消
// 超时继承后可以缩短,但不能延长
// 值传递是链式的,查找是向上遍历
总结
-
Context接口:包含Deadline、Done、Err、Value四个方法
-
四种创建方式:Background、TODO、WithCancel、WithTimeout、WithDeadline、WithValue
-
取消传播:父Context取消,子Context自动取消
-
超时继承:子Context可以缩短但不能延长父Context的超时
-
值传递:使用类型安全的key避免冲突
最佳实践:
-
总是以context.Background()作为根Context
-
函数参数总是传递Context
-
不要传递nil Context
-
HTTP请求和数据库操作必须带Timeout
-
使用包级key类型避免值冲突
-
Context只用于传递请求级数据,不要用于可选参数
💡 下一篇文章我们将深入讲解Go语言的Error处理与errors包,敬请期待!