自己在Go项目开发中遇到的一些的一些规范问题,后续可以封装成Rules供AI编码工具使用
上下文Context
Context透传
Go 里 context.Context 的“透传(pass through)”本质上是一套调用链生命周期管理规范。
核心目标:
- 控制超时 / cancel(异步任务框架可能携带了超时配置、HTTP请求的生命周期管理等)
- 传递 request-scoped 元数据
- 避免 goroutine 泄漏
- 保证链路一致性
以使用Gin作为Web框架为例,从请求API进入时便需要取得请求的上下文ctx,并且在调用service、dao/repo、第三方接口或SDK期间层层透传。
gin.Context
↓
Request.Context()
↓
service(ctx)
↓
dao/repository(ctx)
↓
http/grpc/db/redis/第三方sdk(ctx)
在我遇到的电商项目中就有遇到类似的情况,使用asynq异步队列时,由于代码没有遵从透传ctx的规则,仅透传了所需要的ID
func HandleXXTask(ctx context.Context, task asnyq.Task) {
// parse task payload
xxService.Do(payload.xxID)
}
虽然这样的调用方式看上去没什么问题,service内Redis分布式锁和事务都没有使用这个ctx(未传递或滥用了context.Background()创建了一个新的上下文ctx代替),但是后续就直接引起了超时控制失效和goroutine泄露的问题
- 超时控制失效原因:尽管asynq队列通过asynq.Timeout默认为处理任务的Handle方法配置了超时时间,不巧的是任务处理执行的事务与另一处的行锁加锁顺序是相反的(见前一篇文章提及的建议处理金额任务时固定userID升序或降序,早期的一系列金额处理没有规范顺序),导致了一个事务死锁,新的ctx没有配置超时时间导致方法长时间卡死
- goroutine泄露原因:使用了封装后的便携Redis分布式锁util方法,其中包含一个自动续期锁的goroutine(只能通过ctx.Done接收到结束消息或主动调用Release方法释放),事务的长时间死锁就导致了代码中
defer mutex.Release(ctx)返回时主动释放分布式锁的逻辑不可达。程序中就存在一个泄露的自动续期goroutine,后续任务重试时立刻因为分布式锁被旧的goroutine续期抢占而无法进行补偿
附:goroutine应当监听ctx.Done,避免泄露
go func() {
for {
select {
case <-ctx.Done():
return
default:
}
}
}()
附:分布式锁和事务应当透传ctx,且分布式锁的释放应当使用带超时时间的独立ctx,避免外部ctx超时无法执行释放和释放时抖动导致的混沌问题
// -- 分布式锁处理
key := fmt.Sprintf("xxTaskOp:%v", xxID)
mutex, err := redisUtils.NewLocker().GetAndLock(ctx, key, time.Second*100, 1)
if err != nil {
// 记录日志
return fmt.Errorf("操作正在处理中")
}
defer func() {
// 使用独立 ctx 释放锁, 避免 task ctx 因 asynq.Timeout 取消后无法主动 DEL key
releaseCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if ok, releaseErr := mutex.Release(releaseCtx); releaseErr != nil || !ok {
// 释放失败,记录日志
}
}()
// -- 事务处理
err = global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 一些事务逻辑
}
Context应当携带什么信息
规范:context value 只放 request-scoped metadata
可以放:
- request_id
- trace_id
- auth info
可以放进 ctx 的通常是“横切信息”,不是业务入参。
判断标准很简单:
- 如果这个字段是业务逻辑必须显式依赖的,就放到 req 里。
- 如果这个字段是日志、追踪、超时、取消、链路透传用的,就放到 ctx 里。
userId 多数情况下属于业务参数,建议显式放 req.UserID。
但有一个例外:鉴权中间件可以把当前登录态的轻量信息放 ctx,比如 principal/userID/tenantID,用于通用鉴权、审计、日志。不过进入具体 service 方法时,核心业务参数还是建议显式传入。
(陆续更新中)