[开发笔记]给自己的Golang开发规范

自己在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 方法时,核心业务参数还是建议显式传入。

(陆续更新中)

发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注