上下文(Context)
本章讲解Go Context:Context接口与传播、超时与取消、HTTP/数据库中的应用。重点:Context是Go并发编程的核心,用于跨goroutine传递请求范围的元数据和取消信号。
前置知识:第10章 并发编程 学习目标:理解Context树形传播、掌握超时/取消用法
11.1 Context接口与传播
Section titled “11.1 Context接口与传播”Context是什么
Section titled “Context是什么”Context是Go标准库,用于在goroutine之间传递请求范围的数据和取消信号。
type Context interface { Deadline() (deadline time.Time, ok bool) // 截止时间 Done() <-chan struct{} // 完成信号 Err() error // 错误原因 Value(key any) any // 获取值}创建根Context
Section titled “创建根Context”import "context"
// 空context(根context)ctx := context.Background()
// 可取消的空context(用于测试)ctx := context.TODO()重要:不要传递nil Context。
WithCancel取消
Section titled “WithCancel取消”func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel()
go worker(ctx)
time.Sleep(2 * time.Second) cancel() // 发送取消信号 time.Sleep(time.Second)}
func worker(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("收到取消信号:", ctx.Err()) return default: fmt.Println("工作中...") time.Sleep(200 * time.Millisecond) } }}Context树形传播
Section titled “Context树形传播”func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel()
// 创建子context ctx1, cancel1 := context.WithCancel(ctx) defer cancel1()
ctx2, cancel2 := context.WithCancel(ctx) defer cancel2()
go worker(ctx1, "worker1") go worker(ctx2, "worker2")
time.Sleep(1 * time.Second) cancel() // 取消所有子context time.Sleep(time.Second)}
func worker(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s: 取消 (%v)\n", name, ctx.Err()) return default: fmt.Printf("%s: 工作\n", name) time.Sleep(200 * time.Millisecond) } }}11.2 超时与取消
Section titled “11.2 超时与取消”本节目的:掌握WithTimeout/WithDeadline实现超时控制
WithTimeout超时
Section titled “WithTimeout超时”func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel()
result := make(chan string, 1)
go func() { time.Sleep(3 * time.Second) result <- "完成" }()
select { case res := <-result: fmt.Println("结果:", res) case <-ctx.Done(): fmt.Println("超时:", ctx.Err()) }}WithDeadline截止时间
Section titled “WithDeadline截止时间”// 截止时间:3秒后deadline := time.Now().Add(3 * time.Second)ctx, cancel := context.WithDeadline(context.Background(), deadline)defer cancel()超时应用场景
Section titled “超时应用场景”// 数据库查询超时func queryDB(ctx context.Context, sql string) (*sql.Rows, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel()
return db.QueryContext(ctx, sql)}
// HTTP请求超时func fetchURL(ctx context.Context, url string) ([]byte, error) { req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body)}11.3 在HTTP和数据库中的应用
Section titled “11.3 在HTTP和数据库中的应用”本节目的:掌握Context在实际HTTP请求和数据库操作中的应用
HTTP请求中的Context
Section titled “HTTP请求中的Context”import ( "context" "net/http")
func handler(w http.ResponseWriter, r *http.Request) { // 从请求获取context ctx := r.Context()
// 检查context是否取消 select { case <-time.After(5 * time.Second): w.Write([]byte("OK")) case <-ctx.Done(): fmt.Println("客户端断开") }}
func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil)}传递Context的完整HTTP请求
Section titled “传递Context的完整HTTP请求”func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/users/octocat", nil)
client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("请求失败:", err) return } defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) fmt.Printf("响应: %s\n", string(body[:100]))}数据库操作中的Context
Section titled “数据库操作中的Context”import ( "context" "database/sql")
// 查询超时func queryUser(ctx context.Context, db *sql.DB, id int) (*User, error) { var user User err := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = ?", id, ).Scan(&user.ID, &user.Name)
if err == sql.ErrNoRows { return nil, fmt.Errorf("用户%d不存在", id) } return &user, err}
// 带超时的数据库操作func queryWithTimeout(db *sql.DB) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users LIMIT 10") if err != nil { fmt.Println("查询失败:", err) return } defer rows.Close()
for rows.Next() { // 处理每一行 }}Gin框架中的Context
Section titled “Gin框架中的Context”import ( "github.com/gin-gonic/gin")
func handler(c *gin.Context) { ctx := c.Request.Context()
select { case <-time.After(5 * time.Second): c.JSON(200, gin.H{"message": "OK"}) case <-ctx.Done(): fmt.Println("客户端断开") }}
func main() { r := gin.Default() r.GET("/", handler) r.Run()}传递自定义值
Section titled “传递自定义值”// 定义key类型(避免类型冲突)type ctxKey string
const userKey ctxKey = "user"
// 存储值func storeValue(ctx context.Context, user string) { ctx = context.WithValue(ctx, userKey, user)}
// 获取值func getValue(ctx context.Context) string { if user, ok := ctx.Value(userKey).(string); ok { return user } return ""}完整示例:带超时的爬虫
Section titled “完整示例:带超时的爬虫”func crawlWithTimeout(urls []string, timeout time.Duration) []string { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel()
results := make(chan string, len(urls)) var wg sync.WaitGroup
for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() select { case <-ctx.Done(): return default: if resp, err := http.Get(u); err == nil { resp.Body.Close() results <- u } } }(url) }
go func() { wg.Wait() close(results) }()
var crawled []string for r := range results { crawled = append(crawled, r) } return crawled}- Context作为第一个参数:所有需要取消或超时的函数都应接受Context作为第一个参数
- 不要存储Context:不要把Context存在结构体中,应该在每次调用时传递
- context.Background vs context.TODO:Background用于确定会使用的根Context;TODO用于尚未确定的地方
- 避免Context泄漏:每次创建WithCancel/WithTimeout/WithDeadline都要确保cancel()被调用,或使用defer cancel()
- HTTP请求自动传播:Go的
http.Request包含Context,用http.NewRequestWithContext传递Context
| Python | Go |
|---|---|
| asyncio.CancelledError | context.Canceled |
| 无 | context.DeadlineExceeded |
| 无 | context.WithCancel |
| 无 | context.WithTimeout |
| 无 | context.WithValue |
Context使用规则:
- Context作为第一个参数传递
- 不要在结构体中存储Context
- Context是线程安全的
- 使用context.Background()创建根Context
- 实现一个可取消的goroutine
- 创建一个5秒超时的HTTP请求
- 实现一个带超时的数据库查询
- 用context传播实现链路追踪
- 实现一个生产者-消费者程序,用context取消