下面用几个逐步升级的例子,系统理解 Go 里的 context.Context。
context 主要解决四类问题:
- 取消任务
- 设置超时
- 设置截止时间
- 在调用链中传递请求级数据
核心原则:
context不是用来传普通业务参数的,而是用来控制一条调用链的生命周期。
一、最基础:手动取消任务
先看最简单的例子。
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 收到取消信号:", ctx.Err())
return
default:
fmt.Println("worker 正在工作...")
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(3 * time.Second)
fmt.Println("main 主动取消任务")
cancel()
time.Sleep(time.Second)
}
运行效果大概是:
worker 正在工作...
worker 正在工作...
worker 正在工作...
main 主动取消任务
worker 收到取消信号: context canceled
1. context.Background()
ctx := context.Background()
它是一个空的根上下文,一般用于:
main函数- 后台任务
- 测试代码
- 调用链的最顶层
它不会被取消,也没有超时。
2. context.WithCancel
ctx, cancel := context.WithCancel(context.Background())
返回两个值:
ctx
cancel
调用:
cancel()
之后:
ctx.Done()
对应的 channel 会被关闭。
因此下面的代码会被触发:
case <-ctx.Done():
3. ctx.Err()
取消后:
ctx.Err()
会返回:
context.Canceled
完整判断也可以这样写:
if err := ctx.Err(); err != nil {
fmt.Println(err)
}
二、为什么不用普通的 bool 取消
你可能会想到:
stop := false
然后 goroutine 不断检查:
if stop {
return
}
这种写法有几个问题:
- 有并发读写风险
- 需要加锁或使用原子变量
- 不方便向多个 goroutine 广播
- 不方便形成父子调用链
- 没有统一的超时机制
而 context 的取消信号本质上是关闭一个 channel。
一个 channel 被关闭后,所有监听者都能同时收到通知。
三、一个 Context 取消多个 goroutine
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s 停止工作,原因:%v\n", name, ctx.Err())
return
case <-time.After(time.Second):
fmt.Printf("%s 正在工作\n", name)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "worker-1")
go worker(ctx, "worker-2")
go worker(ctx, "worker-3")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(time.Second)
}
调用一次:
cancel()
三个 goroutine 都会停止。
这就是 context 很重要的一个特性:
广播式取消。
四、使用 WithTimeout 设置超时
很多时候,我们不希望任务无限执行。
例如:
- 查询数据库最多允许 2 秒
- 调用第三方接口最多允许 5 秒
- 文件处理最多允许 10 秒
可以使用:
context.WithTimeout
示例:
package main
import (
"context"
"fmt"
"time"
)
func slowTask(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
fmt.Println("任务执行完成")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
err := slowTask(ctx)
if err != nil {
fmt.Println("任务失败:", err)
}
}
输出:
任务失败: context deadline exceeded
因为:
- 任务本身需要 5 秒
- context 只允许执行 2 秒
- 2 秒后自动取消
此时:
ctx.Err()
返回:
context.DeadlineExceeded
五、为什么一定要 defer cancel()
常见写法:
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
即使 context 会自动超时,也仍然建议调用:
cancel()
因为 context 内部可能维护:
- timer
- 子 context
- 取消通知
- 相关资源
如果函数提前结束,调用 cancel() 可以尽早释放资源。
正确模式:
ctx, cancel := context.WithTimeout(parentCtx, time.Second)
defer cancel()
六、WithDeadline:设置具体截止时间
WithTimeout 表示:
从现在开始,最多运行多久
WithDeadline 表示:
运行到某个具体时间点
示例:
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(
context.Background(),
deadline,
)
defer cancel()
完整示例:
package main
import (
"context"
"fmt"
"time"
)
func main() {
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(
context.Background(),
deadline,
)
defer cancel()
fmt.Println("截止时间:", deadline.Format("15:04:05"))
<-ctx.Done()
fmt.Println("context 结束:", ctx.Err())
}
三秒后:
context 结束: context deadline exceeded
实际项目中,WithTimeout 使用得更多。
七、Context 在函数调用链中传递
真实项目不会只有一个函数,通常是:
Controller
↓
Service
↓
Repository
↓
Database
context 应该沿着整条调用链传递。
示例:
package main
import (
"context"
"fmt"
"time"
)
func controller(ctx context.Context) {
err := service(ctx)
if err != nil {
fmt.Println("controller:", err)
}
}
func service(ctx context.Context) error {
return repository(ctx)
}
func repository(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
fmt.Println("数据库查询完成")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
controller(ctx)
}
执行过程:
main 创建 context
↓
controller 接收 context
↓
service 继续传递
↓
repository 监听 context
两秒后,最底层的 repository 也能收到取消信号。
这就是 context 的核心价值:
在整个调用链中传递取消和超时信号。
八、HTTP 请求中的 Context
在 Go HTTP 服务中,每个请求本身就带有一个 context:
r.Context()
示例:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func queryData(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := queryData(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusRequestTimeout)
return
}
fmt.Fprintln(w, "查询成功")
}
func main() {
http.HandleFunc("/data", handler)
http.ListenAndServe(":8080", nil)
}
当发生以下情况时,请求 context 可能被取消:
- 客户端主动断开连接
- 请求被取消
- HTTP Server 超时
- 上游 context 被取消
因此,业务函数应该优先使用:
r.Context()
而不是重新创建:
context.Background()
错误写法:
func handler(w http.ResponseWriter, r *http.Request) {
// 错误:丢失了 HTTP 请求原本的取消信号
ctx := context.Background()
queryData(ctx)
}
正确写法:
ctx := r.Context()
九、Gin 中如何使用 Context
你使用的是 Gin,需要注意两个不同的 context:
*gin.Context
和:
context.Context
它们不是同一个类型。
Gin Controller 中可以这样获取标准 context:
func GetUser(c *gin.Context) {
ctx := c.Request.Context()
user, err := userService.GetUser(ctx, 1)
if err != nil {
c.JSON(500, gin.H{
"message": err.Error(),
})
return
}
c.JSON(200, user)
}
Service:
func (s *UserService) GetUser(
ctx context.Context,
id uint,
) (*User, error) {
return s.repo.GetUser(ctx, id)
}
Repository:
func (r *UserRepository) GetUser(
ctx context.Context,
id uint,
) (*User, error) {
var user User
err := r.db.
WithContext(ctx).
First(&user, id).
Error
if err != nil {
return nil, err
}
return &user, nil
}
这里最关键的是:
db.WithContext(ctx)
这样,当 HTTP 请求被取消时,数据库操作也有机会停止。
十、GORM 中的 Context
你现在经常使用 GORM。
普通查询:
db.Where("id = ?", id).First(&user)
带 context:
db.WithContext(ctx).
Where("id = ?", id).
First(&user)
完整例子:
func GetUserByID(
ctx context.Context,
db *gorm.DB,
id uint,
) (*User, error) {
var user User
err := db.
WithContext(ctx).
Where("id = ?", id).
First(&user).
Error
if err != nil {
return nil, err
}
return &user, nil
}
还可以额外设置数据库查询超时:
func GetUserByID(
parentCtx context.Context,
db *gorm.DB,
id uint,
) (*User, error) {
ctx, cancel := context.WithTimeout(
parentCtx,
2*time.Second,
)
defer cancel()
var user User
err := db.
WithContext(ctx).
Where("id = ?", id).
First(&user).
Error
if err != nil {
return nil, err
}
return &user, nil
}
这样即使 HTTP 请求没有取消,数据库查询超过两秒也会被取消。
十一、WithValue:传递请求级数据
Context 还可以传递少量请求级数据。
例如:
- request ID
- trace ID
- 当前用户 ID
- 链路追踪信息
- 日志字段
示例:
package main
import (
"context"
"fmt"
)
type contextKey string
const userIDKey contextKey = "userID"
func service(ctx context.Context) {
userID, ok := ctx.Value(userIDKey).(uint)
if !ok {
fmt.Println("没有找到 userID")
return
}
fmt.Println("当前用户 ID:", userID)
}
func main() {
ctx := context.WithValue(
context.Background(),
userIDKey,
uint(1001),
)
service(ctx)
}
输出:
当前用户 ID: 1001
不要直接使用字符串作为 key
不推荐:
ctx := context.WithValue(ctx, "userID", 1001)
因为不同包可能使用同样的字符串,发生冲突。
推荐自定义类型:
type contextKey string
const userIDKey contextKey = "userID"
更严谨的写法:
type contextKey struct {
name string
}
var userIDKey = contextKey{name: "userID"}
十二、封装 Context Value
不要在业务代码里到处写:
ctx.Value(userIDKey).(uint)
建议封装。
package requestctx
import "context"
type key struct {
name string
}
var userIDKey = key{name: "userID"}
func WithUserID(ctx context.Context, userID uint) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func UserID(ctx context.Context) (uint, bool) {
userID, ok := ctx.Value(userIDKey).(uint)
return userID, ok
}
使用:
ctx = requestctx.WithUserID(ctx, 1001)
userID, ok := requestctx.UserID(ctx)
这样可以避免:
- key 冲突
- 类型断言散落
- 不同模块使用不同 key
- 后续修改困难
十三、Gin 中间件传递用户信息
假设 JWT 中间件解析出用户 ID。
可以把用户 ID 放到标准 context 中。
package middleware
import (
"context"
"github.com/gin-gonic/gin"
)
type contextKey struct {
name string
}
var userIDKey = contextKey{name: "userID"}
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 示例:实际项目中从 JWT 解析
userID := uint(1001)
ctx := context.WithValue(
c.Request.Context(),
userIDKey,
userID,
)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
Controller:
func GetProfile(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := ctx.Value(userIDKey).(uint)
if !ok {
c.JSON(401, gin.H{
"message": "未获取到用户信息",
})
return
}
c.JSON(200, gin.H{
"user_id": userID,
})
}
不过在 Gin 项目里,简单数据也经常使用:
c.Set("userID", userID)
然后:
value, exists := c.Get("userID")
二者区别:
gin.Context数据主要在 Controller 和中间件之间使用context.Context数据可以继续传到 Service、Repository、数据库和外部请求
如果用户信息需要深入调用链,标准 context.Context 更合适。
十四、父子 Context 的传播关系
Context 可以形成树结构。
rootCtx
├── childCtx1
└── childCtx2
└── grandChildCtx
示例:
package main
import (
"context"
"fmt"
"time"
)
func watch(ctx context.Context, name string) {
<-ctx.Done()
fmt.Printf("%s 被取消: %v\n", name, ctx.Err())
}
func main() {
parentCtx, parentCancel := context.WithCancel(
context.Background(),
)
childCtx, childCancel := context.WithCancel(parentCtx)
go watch(parentCtx, "parent")
go watch(childCtx, "child")
time.Sleep(time.Second)
fmt.Println("取消 parent")
parentCancel()
time.Sleep(time.Second)
childCancel()
}
取消父 context:
parentCancel()
会同时取消:
- parent
- child
- child 的所有后代
但取消 child:
childCancel()
不会影响 parent。
规则是:
父 context 取消,会向下传播;子 context 取消,不会向上传播。
十五、子 Context 的超时不能超过父 Context
示例:
parentCtx, parentCancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer parentCancel()
childCtx, childCancel := context.WithTimeout(
parentCtx,
10*time.Second,
)
defer childCancel()
虽然 child 设置了 10 秒,但父 context 只有 2 秒。
最终 child 最多只能活 2 秒。
可以理解为:
实际截止时间 = 父截止时间和子截止时间中更早的那个
十六、可取消的循环任务
这是项目中非常常见的写法。
func runMonitor(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("采集设备数据")
case <-ctx.Done():
fmt.Println("停止采集:", ctx.Err())
return
}
}
}
启动:
ctx, cancel := context.WithCancel(context.Background())
go runMonitor(ctx)
time.Sleep(5 * time.Second)
cancel()
这适合:
- MQTT 数据采集
- 设备监控
- 定时同步
- WebSocket 心跳
- 后台轮询
- 消息消费
十七、错误示例:在循环里使用 time.Sleep
下面代码虽然可以工作,但取消响应不及时:
func worker(ctx context.Context) {
for {
if ctx.Err() != nil {
return
}
doWork()
time.Sleep(30 * time.Second)
}
}
假设刚进入 Sleep 后调用了:
cancel()
任务仍然要等待最多 30 秒才能退出。
更好的写法:
func worker(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
doWork()
}
}
}
这样收到取消后,可以立即退出。
十八、HTTP 客户端请求中使用 Context
调用第三方接口时,也应该传入 context。
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://example.com",
nil,
)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应失败:", err)
return
}
fmt.Println(string(body))
}
关键代码:
http.NewRequestWithContext
当 context 超时或取消时,HTTP 请求也会终止。
十九、并发任务:任意一个失败,取消其他任务
假设同时执行三个任务:
- 查询用户
- 查询订单
- 查询权限
只要一个失败,其余任务就没有继续执行的必要。
基础写法:
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
func task(ctx context.Context, name string, duration time.Duration) error {
select {
case <-time.After(duration):
fmt.Println(name, "完成")
return nil
case <-ctx.Done():
fmt.Println(name, "被取消")
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
errChan := make(chan error, 3)
wg.Add(3)
go func() {
defer wg.Done()
err := task(ctx, "用户查询", 2*time.Second)
if err != nil {
errChan <- err
cancel()
}
}()
go func() {
defer wg.Done()
err := task(ctx, "订单查询", 5*time.Second)
if err != nil {
errChan <- err
cancel()
}
}()
go func() {
defer wg.Done()
time.Sleep(time.Second)
errChan <- errors.New("权限查询失败")
cancel()
}()
wg.Wait()
close(errChan)
for err := range errChan {
fmt.Println("错误:", err)
}
}
不过在生产项目里,更推荐使用:
golang.org/x/sync/errgroup
二十、errgroup:并发任务的标准写法
package main
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func task(
ctx context.Context,
name string,
duration time.Duration,
shouldFail bool,
) error {
select {
case <-time.After(duration):
if shouldFail {
return fmt.Errorf("%s 执行失败", name)
}
fmt.Println(name, "执行成功")
return nil
case <-ctx.Done():
fmt.Println(name, "被取消")
return ctx.Err()
}
}
func main() {
ctx := context.Background()
group, ctx := errgroup.WithContext(ctx)
group.Go(func() error {
return task(ctx, "任务A", 2*time.Second, false)
})
group.Go(func() error {
return task(ctx, "任务B", time.Second, true)
})
group.Go(func() error {
return task(ctx, "任务C", 5*time.Second, false)
})
if err := group.Wait(); err != nil {
fmt.Println("任务组失败:", err)
}
}
当任务 B 失败后:
任务B 执行失败
errgroup 会自动取消关联的 context。
任务 A、任务 C 如果还没完成,会收到取消信号。
这是非常实用的并发模式。
二十一、Context 和 Channel 如何配合
Context 负责取消,channel 负责传输数据。
示例:
package main
import (
"context"
"fmt"
"time"
)
func producer(
ctx context.Context,
resultChan chan<- int,
) {
defer close(resultChan)
for i := 1; i <= 100; i++ {
select {
case <-ctx.Done():
fmt.Println("生产者退出:", ctx.Err())
return
case resultChan <- i:
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
resultChan := make(chan int)
go producer(ctx, resultChan)
for value := range resultChan {
fmt.Println("收到数据:", value)
}
}
注意生产者发送数据时也要监听取消:
select {
case <-ctx.Done():
return
case resultChan <- value:
}
否则如果没有消费者,生产者可能永久阻塞在:
resultChan <- value
即使 context 已经取消,也无法退出。
二十二、典型错误:创建 goroutine 后不监听 Context
错误写法:
func worker(ctx context.Context) {
for {
doWork()
}
}
即使上层调用:
cancel()
这个 goroutine 也不会停止。
Context 不会强制杀死 goroutine。
它只是发出一个取消信号。
任务必须主动监听:
select {
case <-ctx.Done():
return
default:
}
这是理解 context 最重要的一点:
Context 是协作式取消,不是强制中断。
二十三、典型错误:把 Context 放到结构体里
一般不推荐:
type UserService struct {
ctx context.Context
}
然后:
service.ctx
因为 context 通常对应某一次请求。
如果把它存进长期存在的 Service 结构体里,可能导致:
- 不同请求共用 context
- 请求数据串扰
- 旧 context 已取消
- 生命周期混乱
推荐作为函数第一个参数传递:
func (s *UserService) GetUser(
ctx context.Context,
id uint,
) (*User, error)
标准约定:
func DoSomething(ctx context.Context, ...)
二十四、典型错误:传递 nil Context
不要这样:
service.GetUser(nil, 1)
标准库很多函数不接受 nil context。
没有合适的 context 时使用:
context.Background()
或者:
context.TODO()
二十五、Background 和 TODO 的区别
context.Background()
表示你明确知道这是一个根 context。
ctx := context.Background()
context.TODO()
表示这里未来应该传入一个合适的 context,但目前还没有完成改造。
ctx := context.TODO()
例如旧代码迁移:
func oldFunction() {
ctx := context.TODO()
newFunction(ctx)
}
生产代码中不要长期大量使用 TODO()。
它更像一个标记:
这里以后还需要完善。
二十六、Context 错误判断
常见错误有两个:
context.Canceled
context.DeadlineExceeded
建议使用:
errors.Is
示例:
if err != nil {
switch {
case errors.Is(err, context.Canceled):
fmt.Println("任务被主动取消")
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("任务执行超时")
default:
fmt.Println("其他错误:", err)
}
}
不要只判断字符串:
if err.Error() == "context canceled"
字符串判断不可靠。
二十七、在 Service 中如何正确处理 Context 错误
假设数据库返回:
context deadline exceeded
一般不应该把它包装得完全无法识别。
可以这样:
func (s *UserService) GetUser(
ctx context.Context,
id uint,
) (*User, error) {
user, err := s.repo.GetUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("查询用户失败: %w", err)
}
return user, nil
}
这里使用:
%w
保留原始错误链。
上层依然可以判断:
errors.Is(err, context.DeadlineExceeded)
二十八、Gin + GORM 完整示例
下面给一个比较接近你实际项目的写法。
Model
package model
import "time"
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;not null" json:"username"`
Nickname string `gorm:"size:64" json:"nickname"`
Status uint8 `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Repository
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"your-project/model"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{
db: db,
}
}
func (r *UserRepository) GetByID(
ctx context.Context,
id uint,
) (*model.User, error) {
var user model.User
err := r.db.
WithContext(ctx).
First(&user, id).
Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
Service
package service
import (
"context"
"errors"
"fmt"
"time"
"your-project/model"
"your-project/repository"
)
var ErrUserNotFound = errors.New("用户不存在")
type UserService struct {
userRepo *repository.UserRepository
}
func NewUserService(
userRepo *repository.UserRepository,
) *UserService {
return &UserService{
userRepo: userRepo,
}
}
func (s *UserService) GetByID(
parentCtx context.Context,
id uint,
) (*model.User, error) {
ctx, cancel := context.WithTimeout(
parentCtx,
3*time.Second,
)
defer cancel()
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("查询用户失败: %w", err)
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
Controller
package controller
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"your-project/service"
)
type UserController struct {
userService *service.UserService
}
func NewUserController(
userService *service.UserService,
) *UserController {
return &UserController{
userService: userService,
}
}
func (ctl *UserController) GetByID(c *gin.Context) {
idValue := c.Param("id")
id64, err := strconv.ParseUint(idValue, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": "用户 ID 不合法",
})
return
}
ctx := c.Request.Context()
user, err := ctl.userService.GetByID(ctx, uint(id64))
if err != nil {
switch {
case errors.Is(err, service.ErrUserNotFound):
c.JSON(http.StatusNotFound, gin.H{
"message": "用户不存在",
})
case errors.Is(err, context.Canceled):
c.JSON(499, gin.H{
"message": "请求已取消",
})
case errors.Is(err, context.DeadlineExceeded):
c.JSON(http.StatusGatewayTimeout, gin.H{
"message": "查询超时",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
"message": "服务器内部错误",
})
}
return
}
c.JSON(http.StatusOK, gin.H{
"data": user,
})
}
这条链路是:
Gin Request Context
↓
Controller
↓
Service 增加 3 秒超时
↓
Repository
↓
GORM WithContext
↓
数据库驱动
二十九、后台任务与请求 Context 的区别
假设用户调用接口后,你启动一个需要长期执行的任务:
func handler(c *gin.Context) {
ctx := c.Request.Context()
go generateReport(ctx)
c.JSON(200, gin.H{
"message": "任务已启动",
})
}
这里可能有问题。
HTTP 请求结束后:
c.Request.Context()
通常会被取消。
于是后台任务可能立刻停止。
如果这个任务必须脱离 HTTP 请求继续执行,就不应该直接使用请求 context。
可以使用:
go generateReport(context.Background())
但这样又失去了统一关闭能力。
更合理的是,应用启动时创建应用级 context:
appCtx, appCancel := context.WithCancel(context.Background())
后台任务使用:
go generateReport(appCtx)
服务关闭时:
appCancel()
判断标准:
- 任务属于当前请求:使用
r.Context() - 任务属于整个应用:使用应用级 context
- 任务属于某个批次:为该批次创建专用 context
三十、优雅关闭服务器
Context 也常用于服务优雅关闭。
package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
}
go func() {
fmt.Println("服务器启动:", server.Addr)
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
signalCtx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stop()
<-signalCtx.Done()
fmt.Println("收到退出信号,开始关闭服务")
shutdownCtx, cancel := context.WithTimeout(
context.Background(),
10*time.Second,
)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
fmt.Println("关闭服务失败:", err)
return
}
fmt.Println("服务器已安全关闭")
}
这里有两个 context:
signalCtx
用于等待系统信号:
- Ctrl+C
- SIGTERM
shutdownCtx
用于限制服务器关闭时间,最多等待 10 秒。
三十一、Context 的四个核心方法
context.Context 接口本身非常简单:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Deadline
获取截止时间:
deadline, ok := ctx.Deadline()
Done
返回取消通知 channel:
<-ctx.Done()
Err
返回取消原因:
ctx.Err()
Value
获取请求级数据:
ctx.Value(key)
三十二、什么时候需要在函数中检查 Context
如果函数调用的是支持 context 的底层 API,例如:
db.WithContext(ctx)
http.NewRequestWithContext(ctx, ...)
底层通常会帮你监听。
但如果你自己写的是 CPU 密集型循环,需要主动检查。
例如:
func calculate(ctx context.Context, count int) error {
for i := 0; i < count; i++ {
if i%1000 == 0 {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
// 执行计算
}
return nil
}
不要每次循环都检查,否则极端情况下会增加开销。
可以每隔一定次数检查一次。
三十三、生产级最佳实践
1. Context 放在第一个参数
推荐:
func QueryUser(
ctx context.Context,
id uint,
) error
不推荐:
func QueryUser(
id uint,
ctx context.Context,
) error
2. 不要把 Context 设为可选参数
不推荐:
func QueryUser(id uint, ctx ...context.Context)
直接明确传入。
3. 不要传 nil
使用:
context.Background()
4. 创建子 Context 后及时 cancel
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
5. 不要用 Context 传业务参数
不推荐:
context.WithValue(ctx, "page", 1)
context.WithValue(ctx, "pageSize", 20)
context.WithValue(ctx, "username", "admin")
这些应该使用结构体:
type UserQuery struct {
Page int
PageSize int
Username string
}
然后:
func ListUsers(
ctx context.Context,
query UserQuery,
) error
6. Value 只传请求级元数据
适合:
- user ID
- trace ID
- request ID
- tenant ID
- 日志字段
不适合:
- 数据库连接
- Service 对象
- 页面参数
- 大结构体
- 可选配置集合
7. 不要主动调用 ctx.Done() 对应 channel 的 close
你只能调用:
cancel()
不能关闭:
close(ctx.Done())
因为 Done() 返回的是只读 channel:
<-chan struct{}
三十四、用一句话理解 Context
可以把 context 想象成一张“任务通行证”。
这张通行证上可能包含:
任务什么时候到期
任务是否已取消
为什么被取消
当前请求属于谁
当前请求的追踪编号
它会从上层一直传到下层:
HTTP 请求
↓
Controller
↓
Service
↓
Repository
↓
数据库 / 第三方 API
任何一层发现通行证失效,都应该尽快停止工作。
三十五、建议你按这个顺序练习
先掌握这五个例子:
1. WithCancel 手动取消 goroutine
2. WithTimeout 自动超时
3. 父 Context 取消多个子任务
4. Gin Request.Context 传到 GORM
5. errgroup 并发任务失败后统一取消
最终记住这个标准模板:
func DoSomething(
ctx context.Context,
) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return nil
}
以及数据库模板:
func Query(
ctx context.Context,
db *gorm.DB,
) error {
return db.
WithContext(ctx).
Find(&result).
Error
}
以及超时模板:
ctx, cancel := context.WithTimeout(
parentCtx,
3*time.Second,
)
defer cancel()
掌握这三个模板,已经能覆盖大部分 Go 项目中的 context 使用场景。