Skip to content

上下文(Context)

本章讲解Go Context:Context接口与传播、超时与取消、HTTP/数据库中的应用。重点:Context是Go并发编程的核心,用于跨goroutine传递请求范围的元数据和取消信号。

前置知识:第10章 并发编程 学习目标:理解Context树形传播、掌握超时/取消用法


Context是Go标准库,用于在goroutine之间传递请求范围的数据和取消信号。

type Context interface {
Deadline() (deadline time.Time, ok bool) // 截止时间
Done() <-chan struct{} // 完成信号
Err() error // 错误原因
Value(key any) any // 获取值
}
import "context"
// 空context(根context)
ctx := context.Background()
// 可取消的空context(用于测试)
ctx := context.TODO()

重要:不要传递nil Context。

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)
}
}
}
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)
}
}
}

本节目的:掌握WithTimeout/WithDeadline实现超时控制

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())
}
}
// 截止时间:3秒后
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 数据库查询超时
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)
}

本节目的:掌握Context在实际HTTP请求和数据库操作中的应用

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)
}
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]))
}
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() {
// 处理每一行
}
}
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()
}
// 定义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 ""
}
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
}

  1. Context作为第一个参数:所有需要取消或超时的函数都应接受Context作为第一个参数
  2. 不要存储Context:不要把Context存在结构体中,应该在每次调用时传递
  3. context.Background vs context.TODO:Background用于确定会使用的根Context;TODO用于尚未确定的地方
  4. 避免Context泄漏:每次创建WithCancel/WithTimeout/WithDeadline都要确保cancel()被调用,或使用defer cancel()
  5. HTTP请求自动传播:Go的http.Request包含Context,用http.NewRequestWithContext传递Context

PythonGo
asyncio.CancelledErrorcontext.Canceled
无context.DeadlineExceeded
无context.WithCancel
无context.WithTimeout
无context.WithValue

Context使用规则:

  1. Context作为第一个参数传递
  2. 不要在结构体中存储Context
  3. Context是线程安全的
  4. 使用context.Background()创建根Context

  1. 实现一个可取消的goroutine
  2. 创建一个5秒超时的HTTP请求
  3. 实现一个带超时的数据库查询
  4. 用context传播实现链路追踪
  5. 实现一个生产者-消费者程序,用context取消