网罗开发 (小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


背景

每次写 GORM 都要写 ctx := context.Background(),真的烦。
那种烦,不是语法层面的,而是一种「视觉疲劳」——同样的三行代码反复出现在每个函数里,心里总会想:

“这真的是 Go 最优雅的写法吗?”

于是我灵机一动,写了个小包来解决这个“啰嗦问题”:

package bc

import (
    "context"
)

var C = context.Background()

然后在业务代码里就可以这样用:

db.WithContext(bc.C).Find(&users)

不再需要每次都写那行烦人的:

ctx := context.Background()

而且还有个小惊喜——保存文件时,Go 工具链会自动帮我加上 import "your_project/bc"
一瞬间我甚至怀疑:这是不是“被官方默许”的做法?

灵机一动的快乐

我承认,这一招刚写出来的时候很爽。
整个项目从此摆脱了无数行重复的 context.Background(),代码清爽了不少。

比如下面是一个典型的 GORM 写法:

ctx := context.Background()
db.WithContext(ctx).Find(&users)

改成这样:

db.WithContext(bc.C).Find(&users)

是不是感觉瞬间轻盈了?
对于那些喜欢“写少一点,看清一点”的 Go 开发者,这种感觉真的有点上头。

DeepSeek 泼的那盆冷水

后来我拿去问 DeepSeek,它直接泼了我一盆冷水:

“这种方法牺牲了上下文机制的所有好处,而且难以测试。”

我当时还挺不服气的:

  • 难测试?不就一个 context.Background() 吗?
  • 真要带超时,用 context.WithTimeout 不就行了吗?

但冷静下来仔细一想,好像还真有点道理。

问题一:全局变量让上下文失去了“传递性”

Go 的 context.Context 设计初衷是在调用链中传递状态
比如超时、取消信号、trace ID、用户信息等,都靠这个 context 一路往下传。

如果你把它替换成一个全局变量 bc.C,那它就不再是“传递”的,而是“固定”的。

举个例子:

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    handler(ctx)
}

func handler(ctx context.Context) {
    service(ctx)
}

func service(ctx context.Context) {
    db.WithContext(ctx).Find(&users)
}

这种写法里,超时、取消信号、链路追踪 ID 都会自动随着 ctx 传递。
而如果你在 service 里直接写:

db.WithContext(bc.C).Find(&users)

那抱歉——上层传来的 context 信息全都丢了。
所有 tracing、timeout、cancel 都被“截断”。

所以,这个“灵机一动”的优化,其实是绕开了 context 的设计初衷
它虽然短,但副作用就是:

“我不关心上层的 context 信息,全都用我自己的 background。”

在一些测试、调试或链路追踪系统里,这就成了灾难。

问题二:“难以测试”其实是可验证的

DeepSeek 说“难测试”,其实是指这个全局 context 没法被 mock 或替换。
举个例子:

你有个函数:

func GetUser(db *gorm.DB) error {
    return db.WithContext(bc.C).First(&User{}).Error
}

在单元测试里,如果你想模拟一个带超时或取消的 context,就做不到了。

比如:

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

你没法把这个 ctx 传给 GetUser,因为它里面写死了 bc.C

而如果函数写成:

func GetUser(ctx context.Context, db *gorm.DB) error {
    return db.WithContext(ctx).First(&User{}).Error
}

那测试时你就可以轻松传不同的上下文,比如 mock 超时、取消或带 trace ID 的 ctx。

总结一下区别:

写法 可测试性 上下文传递 代码简洁度
bc.C 无法传递测试上下文 固定背景 极简
传参式 ctx context.Context 可控制 可继承 稍微啰嗦

有没有折中办法?

有,其实可以通过包装函数来缓解啰嗦问题,又不丢掉上下文机制。

方法一:封装 GORM 操作

比如写一个包装函数,内部自动注入 context.Background(),但仍允许外部传自定义 context:

package dbutil

import (
    "context"
    "gorm.io/gorm"
)

func WithDefaultContext(db *gorm.DB, ctx ...context.Context) *gorm.DB {
    if len(ctx) > 0 {
        return db.WithContext(ctx[0])
    }
    return db.WithContext(context.Background())
}

使用时:

dbutil.WithDefaultContext(db).Find(&users)
// 或者
dbutil.WithDefaultContext(db, myCtx).Find(&users)

这样就两全其美:

  • 默认不传就用 Background()
  • 想要控制时传入自定义 ctx

方法二:在项目级封装

如果你的项目大量用到 GORM,可以直接封一层:

package repo

import (
    "context"
    "gorm.io/gorm"
)

type Repo struct {
    db *gorm.DB
}

func NewRepo(db *gorm.DB) *Repo {
    return &Repo{db: db}
}

func (r *Repo) FindUsers(ctx context.Context) ([]User, error) {
    var users []User
    err := r.db.WithContext(ctx).Find(&users).Error
    return users, err
}

调用端如果不想每次都写 context.Background(),可以这样做:

users, err := repo.FindUsers(context.TODO())

甚至进一步封装一个全局函数:

func DefaultCtx() context.Context {
    return context.Background()
}

然后全项目统一写:

users, err := repo.FindUsers(DefaultCtx())

既保留了上下文传递机制,又兼顾了调用简洁。

总结

那句“灵机一动”的代码:

var C = context.Background()

确实解决了「写太多」的问题,但也的确带来了三个副作用:

  1. 失去了上下文传递能力(超时、取消、trace 全没了);
  2. 难以在测试中替换
  3. 隐式依赖全局变量,不利于扩展。

如果你只是写个小脚本或者一次性任务,用它无伤大雅;
但在中大型项目里,特别是有 tracing、链路日志、超时控制的系统,这种写法就会埋坑。

所以结论其实挺现实的:

如果是“爽一时”的工程师优化,那就用 bc.C
如果要维护一个“能跑十年”的项目,还是老老实实写 ctx := context.Background()

Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐