1. Context概述
Context(上下文)是Go语言中用于管理goroutine生命周期、传递请求范围数据和取消信号的标准包。它在并发编程中起着至关重要的作用。
主要用途:
- 取消操作:主动取消正在执行的操作,即控制 goroutine 退出(取消信号)
- 超时控制:设置操作的最长执行时间
- 传递数据:在goroutine之间安全传递请求范围的数据
- 截止时间:设置操作的绝对截止时间
2. Context核心接口
context.Context 是一个接口,定义了四个方法,用于在 goroutine 之间传递上下文信息:
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回上下文的截止时间(若有)
Done() <-chan struct{} // 返回一个通道,关闭时表示上下文已取消/超时
Err() error // 返回上下文结束的原因(取消/超时)
Value(key any) any // 获取上下文存储的键值对
}
3. Context的创建与使用
context 包提供了多个函数用于创建基础上下文或派生新上下文:
(1)基础Context
context.Background():最顶层的空上下文,通常作为所有上下文的根,不可取消、无截止时间、无值。context.TODO():用于不确定使用哪种上下文的场景,语义上与Background一致,作为占位符。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建根Context
ctx := context.Background()
// 或者使用TODO(当不确定使用哪种Context时)
// ctx := context.TODO()
processRequest(ctx)
}
func processRequest(ctx context.Context) {
// 检查Context是否已取消
select {
case <-ctx.Done():
fmt.Println("Context cancelled:", ctx.Err())
return
default:
fmt.Println("Processing request...")
}
}
(2)可取消的Context
-
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)派生一个可取消的上下文,调用cancel函数会关闭ctx.Done()通道,通知子上下文退出。示例:
func main() {
// 创建可取消的Context
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "worker1")
go worker(ctx, "worker2")
// 3秒后取消所有worker
time.Sleep(3 * time.Second)
fmt.Println("Cancelling all workers...")
cancel()
// 给worker一些时间来处理取消
time.Sleep(1 * time.Second)
}
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: received cancellation signal: %v\n", name, ctx.Err())
return
default:
// 执行任务
fmt.Printf("%s: working...\n", name)
time.Sleep(500 * time.Millisecond)
}
}
}
(3)带超时的Context
-
context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)派生一个超时自动取消的上下文,timeout后自动关闭Done()通道,Err()返回context.DeadlineExceeded。 -
示例:
func main() {
// 设置2秒超时
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 良好实践:总是调用cancel(),,确保资源释放,即使提前完成
go timeConsumingOperation(ctx)
select {
case <-ctx.Done():
fmt.Println("Main: operation completed or timed out")
}
}
func timeConsumingOperation(ctx context.Context) {
for i := 0; i < 5; i++ {
select {
case <-ctx.Done():
fmt.Println("Operation cancelled:", ctx.Err())
return
default:
fmt.Printf("Operation step %d...\n", i+1)
time.Sleep(1 * time.Second)
}
}
fmt.Println("Operation completed successfully")
}
(4)带截止时间的Context
context.WithDeadline(parent Context, deadline time.Time) (ctx Context, cancel CancelFunc)与WithTimeout类似,但直接指定截止时间(deadline)而非超时 duration。- 示例:
func main() {
// 设置具体截止时间
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel() // 良好实践:总是调用cancel(),,确保资源释放,即使提前完成
if dl, ok := ctx.Deadline(); ok {
fmt.Printf("Operation must complete before: %v\n", dl)
}
go processWithDeadline(ctx)
time.Sleep(4 * time.Second) // 等待操作完成或超时
}
func processWithDeadline(ctx context.Context) {
for i := 0; i < 10; i++ {
if ctx.Err() != nil {
fmt.Println("Stopping due to:", ctx.Err())
return
}
fmt.Printf("Processing item %d...\n", i+1)
time.Sleep(500 * time.Millisecond)
}
fmt.Println("All items processed")
}
(5)带值的Context
-
context.WithValue(parent Context, key, val any) Context派生一个携带键值对的上下文,用于传递请求范围的元数据(如认证信息)。注意:
- 键(key)应定义为自定义类型(避免与其他包的键冲突)。
- 不建议传递大量数据,仅用于轻量元数据。
示例:
package main
import (
"context"
"fmt"
)
type keyType string
func main() {
// 创建带值的context
ctx := context.WithValue(context.Background(), keyType("userID"), "123456")
ctx = context.WithValue(ctx, keyType("authToken"), "abc@123456")
processUserRequest(ctx)
}
// 处理用户请求
func processUserRequest(ctx context.Context) {
// 从 context 中获取值
if userID, ok := ctx.Value(keyType("userID")).(string); ok {
fmt.Printf("获取用户ID: %s\n", userID)
}
if authToken, ok := ctx.Value(keyType("authToken")).(string); ok {
fmt.Printf("获取token: %s\n", authToken)
}
// 尝试获取不存在的Key
if value := ctx.Value(keyType("value")); value == nil {
fmt.Println("键“nonExistent”在上下文中不存在")
}
}
4. Context的传播与取消机制
- 层级关系:上下文通过
parent参数形成树状结构,父上下文取消时,所有派生的子上下文都会被取消。 - 取消传递:调用父上下文的
cancel函数,或父上下文超时 / 截止时间到达,会触发所有子上下文的Done()通道关闭。 - 资源释放:
WithCancel、WithTimeout、WithDeadline返回的cancel函数必须被调用(通常用defer),否则可能导致 goroutine 泄漏。
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
// 创建带超时的Context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 创建HTTP请求并附加Context
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/5", nil)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request error: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %v\n", err)
return
}
fmt.Printf("Response received: %s\n", string(body))
}
func queryDatabase(ctx context.Context, query string) ([]string, error) {
// 模拟数据库查询
results := make(chan []string, 1)
go func() {
// 模拟长时间运行的操作
time.Sleep(2 * time.Second)
results <- []string{"result1", "result2", "result3"}
}()
select {
case <-ctx.Done():
return nil, fmt.Errorf("query cancelled: %v", ctx.Err())
case res := <-results:
return res, nil
}
}
func main() {
parentCtx, parentCancel := context.WithCancel(context.Background())
defer parentCancel()
// 创建子Context
childCtx, childCancel := context.WithCancel(parentCtx)
defer childCancel()
go worker(parentCtx, "parent-worker")
go worker(childCtx, "child-worker")
// 3秒后取消父Context,子Context也会被取消
time.Sleep(3 * time.Second)
fmt.Println("Cancelling parent context...")
parentCancel()
time.Sleep(1 * time.Second)
}
5. 最佳实践
-
作为函数首参:需要控制生命周期的函数,应将
context.Context作为第一个参数(命名为ctx)。
// 良好实践:Context作为函数的第一个参数
func processData(ctx context.Context, data []byte) error {
// 定期检查Context是否已取消
for i := range data {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 继续处理
}
// 处理数据...
if i % 1000 == 0 {
// 在长时间运行的操作中定期检查取消
if ctx.Err() != nil {
return ctx.Err()
}
}
}
return nil
}
-
避免传递
nil上下文:始终使用context.Background()或context.TODO()作为根上下文。 -
不存储上下文:不要将
Context作为结构体字段,应在函数间显式传递。 -
优先使用取消信号:goroutine 应通过监听
ctx.Done()优雅退出,而非使用sync.WaitGroup强制阻塞。 -
慎用
WithValue:仅传递必要的元数据,避免滥用导致代码可读性下降。
package main
import (
"context"
"fmt"
)
// 定义Context key的类型安全方式
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
func setContextValues(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, userIDKey, "user-123")
ctx = context.WithValue(ctx, requestIDKey, "req-456")
return ctx
}
func getContextValues(ctx context.Context) {
if userID, ok := ctx.Value(userIDKey).(string); ok {
fmt.Printf("User ID: %s\n", userID)
}
if requestID, ok := ctx.Value(requestIDKey).(string); ok {
fmt.Printf("Request ID: %s\n", requestID)
}
}
6. 常见陷阱和注意事项
- 不要存储Context在结构体中:Context应该作为参数传递
- 总是调用cancel函数:避免内存泄漏
- Context是不可变的:每次WithXXX都返回新的Context
- Context值应该是不变的:存储的值应该是线程安全的
- 使用类型安全的key:避免key冲突
- 在长时间任务中未检查
ctx.Done(),导致无法及时响应取消信号
7. 总结
context 包是 Go 并发编程中管理 goroutine 生命周期的核心工具,通过层级传递取消信号、超时时间和元数据,确保并发任务的可控性和资源安全。合理使用 context 能显著提升代码的健壮性,尤其在分布式系统和高并发场景中不可或缺。
举例说明:使用类型安全的Context Key,避免Key冲突
1. 概念解释
"使用类型安全的key"指的是在Context中存储值时,使用自定义类型作为key,而不是直接使用字符串。这样可以避免不同包或模块中相同字符串key导致的意外覆盖。
2. 问题演示
(1)不安全的字符串Key(可能产生冲突)
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
// 用户服务设置用户ID
ctx = context.WithValue(ctx, "id", "user-123")
// 订单服务也使用"id"作为key
ctx = context.WithValue(ctx, "id", "order-456")
// 冲突!用户ID被订单ID覆盖了
fmt.Printf("ID: %s\n", ctx.Value("id")) // 输出: ID: order-456
}
(2)类型安全的Key(推荐做法)
package main
import (
"context"
"fmt"
)
// 定义不同的类型作为key
type userKey string
type orderKey string
const (
UserIDKey userKey = "id"
OrderIDKey orderKey = "id"
)
func main() {
ctx := context.Background()
// 用户服务使用userKey类型
ctx = context.WithValue(ctx, UserIDKey, "user-123")
// 订单服务使用orderKey类型
ctx = context.WithValue(ctx, OrderIDKey, "order-456")
// 没有冲突!因为key的类型不同
fmt.Printf("User ID: %s\n", ctx.Value(UserIDKey)) // 输出: User ID: user-123
fmt.Printf("Order ID: %s\n", ctx.Value(OrderIDKey)) // 输出: Order ID: order-456
}
3. 实际项目中的完整示例
(1)场景:Web应用中的多模块Context使用
package main
import (
"context"
"fmt"
"net/http"
)
// 定义各个模块的key类型
type (
userKey struct{} // 空结构体,更节省内存
authKey struct{}
requestKey struct{}
)
// 对应的key常量
var (
UserKey = userKey{}
AuthKey = authKey{}
RequestKey = requestKey{}
)
// 用户信息
type User struct {
ID string
Name string
Email string
}
// 认证信息
type AuthInfo struct {
Token string
ExpiresAt int64
}
// 请求信息
type RequestInfo struct {
IP string
UserAgent string
}
// 中间件:设置用户信息
func userMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 模拟从数据库或会话中获取用户信息
user := &User{
ID: "user-123",
Name: "张三",
Email: "zhangsan@example.com",
}
ctx = context.WithValue(ctx, UserKey, user)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 中间件:设置认证信息
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
authInfo := &AuthInfo{
Token: "bearer-abc123",
ExpiresAt: 1672531200,
}
ctx = context.WithValue(ctx, AuthKey, authInfo)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 中间件:设置请求信息
func requestMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqInfo := &RequestInfo{
IP: "192.168.1.100",
UserAgent: r.UserAgent(),
}
ctx = context.WithValue(ctx, RequestKey, reqInfo)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 业务处理函数
func businessHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 安全地从Context中获取各种信息
if user, ok := ctx.Value(UserKey).(*User); ok {
fmt.Fprintf(w, "用户: %s (ID: %s)\n", user.Name, user.ID)
}
if auth, ok := ctx.Value(AuthKey).(*AuthInfo); ok {
fmt.Fprintf(w, "认证Token: %s\n", auth.Token)
}
if req, ok := ctx.Value(RequestKey).(*RequestInfo); ok {
fmt.Fprintf(w, "客户端IP: %s\n", req.IP)
}
}
func main() {
// 设置HTTP路由
handler := http.HandlerFunc(businessHandler)
handler = userMiddleware(handler)
handler = authMiddleware(handler)
handler = requestMiddleware(handler)
http.Handle("/", handler)
fmt.Println("服务器启动在 :8080")
http.ListenAndServe(":8080", nil)
}
(2)更复杂的类型安全Key示例
package main
import (
"context"
"fmt"
)
// 使用带命名空间的key类型
type (
DatabaseKey string
CacheKey string
ConfigKey string
)
// 预定义的key常量
const (
DBConnKey DatabaseKey = "connection"
DBTimeoutKey DatabaseKey = "timeout"
RedisConnKey CacheKey = "redis"
MemcacheKey CacheKey = "memcache"
AppConfigKey ConfigKey = "app"
LogConfigKey ConfigKey = "log"
)
func setupContext() context.Context {
ctx := context.Background()
// 设置数据库相关配置
ctx = context.WithValue(ctx, DBConnKey, "mysql://user:pass@localhost/db")
ctx = context.WithValue(ctx, DBTimeoutKey, 30)
// 设置缓存相关配置
ctx = context.WithValue(ctx, RedisConnKey, "redis://localhost:6379")
ctx = context.WithValue(ctx, MemcacheKey, "memcache://localhost:11211")
// 设置应用配置
ctx = context.WithValue(ctx, AppConfigKey, map[string]interface{}{
"name": "myapp",
"version": "1.0.0",
})
return ctx
}
func processRequest(ctx context.Context) {
// 安全地获取各种配置,不会发生key冲突
if dbConn, ok := ctx.Value(DBConnKey).(string); ok {
fmt.Printf("数据库连接: %s\n", dbConn)
}
if timeout, ok := ctx.Value(DBTimeoutKey).(int); ok {
fmt.Printf("数据库超时: %d秒\n", timeout)
}
if redisConn, ok := ctx.Value(RedisConnKey).(string); ok {
fmt.Printf("Redis连接: %s\n", redisConn)
}
if config, ok := ctx.Value(AppConfigKey).(map[string]interface{}); ok {
fmt.Printf("应用名称: %s\n", config["name"])
}
}
func main() {
ctx := setupContext()
processRequest(ctx)
}
4. 类型安全Key的优势
- 避免命名冲突:不同包中的相同字符串不会冲突
- 编译时类型检查:Go编译器可以帮助发现类型错误
- 更好的IDE支持:代码补全和导航更准确
- 明确的意图:从key的类型就能知道值的用途
- 重构友好:重命名类型时编译器会报错
5. 推荐的Key定义方式
// 方式1:使用空结构体(最节省内存)
type userKey struct{}
var UserKey = userKey{}
// 方式2:使用字符串类型(有描述性)
type configKey string
const DatabaseConfigKey configKey = "database"
// 方式3:使用整数类型(性能最好)
type keyType int
const (
UserKey keyType = iota
AuthKey
ConfigKey
)
总结:使用类型安全的key是Go语言Context的最佳实践,它能有效避免不同模块间的key冲突,提高代码的健壮性和可维护性。