恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Go 项目中的 common 包:为何失控、如何拆分为职责清晰的内聚包

  • 首页
  • 资讯中心
  • /
  • Go 项目中的 common 包:为何失控、如何拆分为职责清晰的内聚包

相关资讯

SpringBoot+Vue校园商铺管理系统开发实战:从技术选型到部署 2026/10/9 4:43:12
华为HCNA-VC认证H11-851题库拆解:高频考点与实操验证 2026/10/9 4:43:12
ESP32C3打造本地化Wi-Fi万能红外遥控器:从硬件到Web控制全解析 2026/10/9 4:43:12

最新资讯

自由职业者AI工具箱:一个人如何把活干完、把钱收回来
MySQL分库分表的三道硬指标与分片策略实战指南
基于JSP+Servlet+MySQL的学生选课系统部署与实现
检测数据清洗格式转换:手工改完说不清动了哪一处怎么办
ponytail插件与skill体系:用“扎带”模型实现信息捕获与流程自动化
Claude Opus 5.5 震撼发布:AI 竞争下半场,真正的护城河在哪?

今日推荐

AI编程智能体实战:从写代码到指挥代码的架构与落地
多模态大模型全栈能力拆解:从数据对齐到弹性推理
大模型Agent开发入门:从工具调用循环到落地避坑指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Go 项目中的 common 包:为何失控、如何拆分为职责清晰的内聚包

发布时间:2026/10/9 4:43:12
Go 项目中的 common 包:为何失控、如何拆分为职责清晰的内聚包 如果你在一个跨了两个月以上的 Go 项目里写过代码大概率已经被common目录折磨过。它一开始可能只是项目根目录下的一个common.go放着一个FormatTime函数后来有人把 HTTP 响应包装丢进去有人把随机串生成丢进去有人把统一错误码丢进去等到项目里所有模块都import xxx/common的时候它已经成了一个谁都不敢动、也说不清楚里面有什么的沼泽。这篇文章想认真讨论 Go 包命名里最常见的反模式common包。我会结合一次商品溯源后端项目的重构过程讲清楚它为什么会失控、破坏了什么以及把它拆成职责清晰的内聚包时具体该走哪几步。适合项目做到一半、开始被 common 反噬的 Go 工程师参考也适合想在代码评审中阻止新 common 出现的人当作依据。1. 先别急着喷命名common 包是怎么一步步失控的1.1 每个人都不会拒绝的“先放这儿”几乎所有 Go 项目里的 common 都是从一个小文件开始的。项目早期只有三五个结构体、一两个工具函数当时把ParseTime放在业务包里也行但你觉得它“以后可能到处用”于是在根目录建了一个common.go。这确实是最短路径。然后下一个同事需要RandomString他打开目录发现common.go已经存在于是顺手再加一个rand.go。再下一个人需要WriteJSON再加response.go。这种模式一旦建立整个团队就形成了一种路径依赖凡是不能一眼确定归属的代码一律放common。我见过最夸张的common包里同时存在这些内容common/ errors.go // 全局错误码定义 response.go // 统一HTTP响应结构 time.go // 时间格式化 rand.go // 随机串生成 crypto.go // 各种加解密辅助 model.go // 通用请求/响应模型 paging.go // 分页参数 config.go // 配置读取 logging.go // 日志初始化 context.go // 上下文工具看起来每个文件都在做不同的事但把它们放进同一个包之后这个包就变得什么都不是。一个包里塞 10 个不同主题的文件时它就是垃圾桶。这里的核心问题不是文件太多而是包名没有表达任何意义。Go 里包名不只是引用路径它是 API 的第一层文档。别人看到common.Foo无法判断它属于哪个领域、依赖什么、可以改到什么程度。1.2 为什么当时那么香复用冲动和避免思考站在创作者心理看common 包能流行是因为它把“决策”延后了。写代码本身就是不断做决策而决定一段代码的归属需要你先想清楚它的职责边界。很多人不想现在就拍板说这段代码属于时间处理还是 HTTP 响应层次于是干脆丢进一个公共区以后再说。这个思路本身不算错误等代码量大了再重构在很多语言里也常见。但 Go 的包机制放大了这种拖延的代价。Go 的包是编译单元、API 边界、测试单元、依赖边界的多重综合体。它不像 Java 的 class 或者 JS 的 module 那样灵活没有protected之类的细粒度隔离跨包访问的唯一手段就是导出。一旦一个函数被放入common并导出整个项目都能看到它这个 API 就会立刻被广泛使用形成公共承诺。改它的签名可能影响几十个文件删它可能需要先说服所有人不再使用。这就导致一个恶性循环common 越来越大 - 越没人敢动 - 越容易往里面放新代码 - 越来越大。等到你想救它的时候它已经不只是命名问题而是结构性债务问题。1.3 关键认知包名决定依赖方向我提一个很多人没有意识到的点给包命名时其实是在为依赖关系投票。common这个名字天然暗示它“被所有人依赖”。所有业务包往它身上指它就成了依赖图的中心节点。中心节点一旦变大大概率会在某个时刻尝试依赖别的领域包于是循环依赖出现。举个例子。你有一个商户模块merchant一个订单模块order它们都 importcommon。后来common里需要判断当前用户是不是某个商户的管理员于是common里 import 了auth。而auth又 import 了merchant。这个时候恶心的事情就发生了merchant-common-auth-merchant。Go 编译直接报 import cycle。面对循环依赖标准的解法是调整结构。但common这个包因为所有东西都混在一起你很难判断到底是谁依赖了谁最后只能把auth里的 User 结构或常量也搬进common问题进一步恶化。所以common不只是审美问题它会在依赖设计上拖垮你。2. 当一个包叫 common 以后它到底破坏了什么2.1 依赖图中的“毛线球”效应正常项目的依赖图应当是分层或分域的领域包之间通过接口交互基础设施位于底层。你可以清楚地说出订单模块依赖商品模块、不依赖支付模块。但如果你的项目有一个common结果就是大家都指向它依赖图读不出来任何有意义的结构。你要分析一次改动影响范围只能全量回归。我在项目里实际遇到过改一个common/time.go里的时区转换函数重新编译完整后端集成测试全绿但所有依赖它的模块都要重算构建缓存。更糟糕的是这种依赖图让测试变得很贵。单元测试本来应该只加载被测包和它的直接依赖。只要被测包 import 了common而common越来越大、最终 import 了半个项目你的单测编译时间、运行时间、mock 复杂度都会跟着上涨。你会发现一个很反常的现象改一行common代码所有模块的测试都触发重新编译。这不是 Go 慢是包边界设计失效导致的。2.2 无法回答“这段代码放哪里”的问题新同事入职看到项目里有个common他遇到任何无法两秒内判断归属的代码都会放进去。大家都会指责他却没人能告诉他在没有common的情况下应该放哪。这就是垃圾桶包的自我强化机制只要垃圾桶存在垃圾就一定会继续增加。更隐蔽的问题是可读性。读代码时order.Settle()你大概能猜出是在结算订单merchant.Verify()能猜出是商户校验而common.ParseTimeString你只知道是通用工具但它所在的模块、它的边界条件、它和time标准库的关系全都不清楚。每一个common.前缀都在迫使读者中断当前思路去查实现。2.3 公共 API 被污染如果你的项目是给第三方用的 SDK问题就更严重。Go 语言没有internal限制之外的隐藏 API 机制common目录一旦放进pkg/或直接放在库根目录里面所有导出符号都会出现在godoc里。别人看你的 SDK 文档时会看到几百个从common包里导出的函数但它们既不内聚也无关紧要。这等同于把你最不想公开的“杂物间”敞开给所有用户看。即便是内部服务common 也会污染公共 API。因为它实际上是一个什么都可以导出的包代码评审很难拒绝新增导出函数。一个包应该有一个可描述的职责包名是名词它能回答“这个包里装的是什么”。common回答不了。2.4 循环依赖只是最明显的暴雷点循环依赖是最容易感知的报错但其实在它出现之前common 已经在出卖你改错成本高同一个包内有太多无关联代码跨包调用点散落各处改一个导出函数很容易漏改。无法单独演进common 里的错误码想改一个版本全项目都要跟着变没有稳定边界。团队并行开发冲突谁都要动 commongit 冲突集中在同一个目录合并时互相踩。表现健康项目有 common 包的项目新代码归属判断按领域/职责能快速决定全都“先丢common”依赖方向分层清晰所有模块指向一个中心包函数可读性timefmt.Parse可理解common.Parse需查实现修改影响范围限定子包全量波及重构难度可以增量演进越积越厚没人敢动3. 动手前先判断你的 common 包还有没有救3.1 三个必须拆分的高危信号不是所有 common 都必须立刻拆但出现以下三个信号意味着它已经进入失控期文件超过 8 个。文件数量本身不是标准但如果超过 8 个且每个主题都不同说明这个包丢失了内聚性。包内代码行数超过 3000 行。工具函数、类型定义、业务逻辑混在一起时重构成本会指数上升。趁早分。你没法在 5 分钟内说出“包里都有什么”。这是最容易判断的标准离开 IDE拿一张纸尝试列出 common 包含的东西。列不出就该拆。还有一个更客观的验证方法用go list -deps列出所有编译单元的依赖然后在结果里统计 common 被多少包引用。go list -deps ./... | grep /common | sort | uniq -c | sort -rn这个命令输出所有模块的依赖包如果 common 出现几十次那它就是项目依赖图的绝对中心。中心度越高越需要立刻处理。3.2 三种处理策略按项目阶段选阶段常见情况推荐策略早期文件很少common 只有 2~3 个文件立刻迁移到具体包成本最低中期文件较多已明显失控但项目在迭代分模块迁移先把依赖最少的函数拆走晚期代码量大循环依赖严重重构窗口小先改名 internal/common 止血再逐步拆分对外 SDKcommon 已在公共路径导出必须拆且要提前规划兼容思路如果团队认为现在没空重构你可以做一个中间动作把common目录从pkg/common移动到internal/common。这一步成本很低但至少阻止了外部使用者把 common 当作公共 API 依赖。注意这只是止血不是解药。移动以后它仍然是内部的一个大垃圾桶仍然会继续吸收新代码所以你必须同时设立一个“不再新增文件”的规则。3.3 拆包的顺序从依赖关系最干净的符号开始拆 common 不能靠一把梭哈。建议按以下顺序先把没有任何业务依赖的纯工具函数拆走时间、随机串、加解密、字符串处理。再拆 HTTP 层的东西响应结构、中间件、错误码。最后拆业务相关的模型和常量因为它们往往牵扯最多模块。每拆出一组就立刻跑测试并提交保持项目一直处于可发布的绿线状态。这个流程能最大限度降低重构恐惧也让团队逐步看到收益。4. 一次完整拆除实录商品溯源项目里的 common 包4.1 我参与的真实项目长什么样我在某个商品溯源后端项目里接手过这样一个 common 包。项目核心逻辑是处理商品的批次溯源、扫码查询、商户认领结构上有独立的领域服务。听起来边界应该很清晰但 common 把一摊东西全混在一起了。当时的目录结构大致是pkg/common/ errors.go // 错误码和错误响应 response.go // HTTP JSON 响应包装 time.go // 时间格式化、时区转换 rand.go // 随机串生成 crypto.go // AES 加密、MD5 工具 model.go // 通用请求/响应模型 middleware.go // 日志、链路ID中间件 config.go // 配置读取封装业务模块是这样的internal/ scanner/ // 扫码查询 merchant/ // 商户管理 product/ // 商品批次 trace/ // 溯源服务每个业务模块的代码里到处是这种调用resp : common.Success(data) ts : common.FormatTime(t, common.TimeZoneAsiaShanghai) code : common.GenRandomCode(6) encrypted, _ : common.AesEncrypt(plaintext, key)我当时的第一反应是这不只是命名问题common 实际上已经承担了 HTTP 层、基础设施层、业务公共层三个角色。整个项目的领域边界全被它抹平了。4.2 第一步新建目标包新旧共存我没有一上来就删除 common而是先建了几个目标包internal/httpreply/ // 响应包装、通用请求模型 internal/timefmt/ // 时间格式化、时区 internal/randstr/ // 随机串 internal/crypto/ // 加解密封装 internal/errcode/ // 错误码 internal/middleware/ // 中间件然后把 common 中的函数按职责复制过去。注意是复制不是直接移动。这样一个中间态里新旧两个包可以共存整个项目不会突然断掉。完成之后我开始逐个模块改引用// 之前 resp : common.Success(data) ts : common.FormatTime(t, common.TimeZoneAsiaShanghai) code : common.GenRandomCode(6) // 之后 resp : httpreply.Success(data) ts : timefmt.FormatTime(t, timefmt.TimeZoneAsiaShanghai) code : randstr.GenCode(6)光看这三个例子你应该已经能感受到差异httpreply.Success让你知道这是 HTTP 响应层的东西timefmt.FormatTime让你知道这是时间格式化randstr.GenCode让你知道这是随机码。每个包名都是一个领域提示词代码的可读性和可定位性提升非常明显。4.3 第二步按模块逐个迁移别想一天做完迁移 common 最容易犯的错误是一口气改完所有调用点。我的做法是按模块划分批次第一周只动scanner模块第二周动merchant第三周动product和trace。每个批次结束后都要跑一次全量测试。为什么这样慢一点反而快因为一旦引入编译错误或行为变化你可以把问题缩小到刚刚改过的模块里而不是在几十个文件里找一处 common 引用。迁移时我用一个简单的查找命令辅助定位grep -rn common\. internal/ --include*.go | head -50common.出现的频率越高越说明这个包被滥用得严重。等所有业务模块都不再引用 common 之后最后一次git rm pkg/common就完成了清理。这一步走完删除目录本身几乎没有任何风险因为项目里已经没有任何编译单元依赖它。4.4 第三步循环依赖是在迁移中暴露出来的正好处理这次迁移中最让我印象深刻的是 middleware。原来它放在 common 里但它需要读取商户ID而 merchant 模块又依赖 common 的响应包装所以形成了merchant - common - merchant的循环依赖。当时项目是怎么绕过循环依赖的非常丑把商户ID的读取代码也塞进了 common。处理这类问题关键是让中间件依赖接口或上下文而不是依赖具体业务包。我把读取商户ID的逻辑改成从context.Context中取值// internal/middleware/merchant.go package middleware import context type merchantIDKey struct{} func WithMerchantID(ctx context.Context, id int64) context.Context { return context.WithValue(ctx, merchantIDKey{}, id) } func MerchantIDFromContext(ctx context.Context) (int64, bool) { id, ok : ctx.Value(merchantIDKey{}).(int64) return id, ok }这样以来middleware 包不再依赖 merchant 包循环依赖链条被切断。业务包只需要在入口处把商户ID放入 context中间件从 context 里取出来用。这是一种非常典型的解耦手段跨包共享的东西不要通过直接的类型依赖传递优先通过接口或 context 携带把依赖关系反转。4.5 迁移后的效果体感比数据更明显维度迁移前迁移后公共包数量1 个 common上千行6 个职责明确的 internal 包新代码归属默认放 common按函数用途放对应包循环依赖数量2 处0 处单测编译时间明显变慢每个模块只编译自己依赖的包快很多可能有人觉得这是小事但只有经历过的人知道common 消失掉之后代码评审才真正可能开始谈边界。以前 review 看到新文件丢进 common 都没动力反对因为它已经是个垃圾堆现在新文件出现在httpreply或timefmt里所有人都能一眼看出合不合理。5. 不叫 common那到底该叫什么一套可落地的命名思路5.1 最简单的原则包名必须能回答“这是什么”给 Go 包命名我一般用三个问题进行测试这个名字能否作为 godoc 的标题httpreply可以common不行。从包名能不能猜到它提供哪些主要类型或函数randstr提供随机字符串相关timefmt提供时间格式化可以。如果项目里同时出现两个同名包是否能通过完整 import 路径区分common不能。命名的时候优先考虑领域名词或能力名词。比如你做支付那pay、settlement、refund都是好的领域包如果是一些横切能力retry、ratelimit、idgen、trace也是好的能力包。你不需要起一个又长又具体的名字Go 风格本来就鼓励短小、小写、单个词。5.2 原 common 里最常见的几类内容到底应该放哪原 common 内容替代包名为什么这样命名统一错误码errcode和错误码相关不掺别的HTTP 响应包装httpreply明确是HTTP响应封装时间格式化timefmt专门处理时间的格式化随机串生成randstr随机字符串这一个能力AES/MD5 等封装crypto加解密能力包日志初始化logging日志相关重试策略retry重试是一个能力限流ratelimit限流是一个能力通用模型放到业务域包模型应该归属于领域配置读取config配置读取这一个能力注意我不是说每个函数都必须有一个独立包。如果某个函数真的很基础比如MinInt、MaxInt你完全可以放在一个叫intmath或者xtype的包里。这样虽然也是工具包但它的名字仍能表达“这是整数数学扩展”而不是无所不包的 common。5.3 如果担心过度拆分用“调用方就近”兜底有些团队问common 拆完之后工具函数变得很多很碎会不会反而过度设计比如 randstr 只有两个函数还要单独一个包是不是有点傻我的答案是如果只有两个函数且只有一处调用那就别开包了——把它放进调用方所在包的未导出函数里。等确认有第二处调用时再提成独立包。这个策略我称之为“调用方就近”代码先跟着调用方走确认了共享需求再公开。这样做的最大好处是你永远不会为了一个只有一处调用的函数创建一个公共包也就自然没有动力去建 common。等包内的工具函数积累到一定程度再按职责抽取整个过程是渐进式、自然发生的。5.4 靠评审规则把 common 新代码挡在门外团队改掉习惯的最有效方法是在代码评审里明确拒绝。可以约定三条规则新代码禁止 import 全局 common 包新增包时包名必须能在命名讨论中说清楚“这个包提供了什么能力”如果一段代码暂时找不到归属先放进调用方的模块内部而不是全局公共区。还可以写一个简单的 grep 检查接到 CI 或 pre-commit 钩子里#!/bin/sh # 防止新代码引入 common 包引用路径需要按项目实际情况调整 if grep -rn import.*/common\ --include*.go . ; then echo Detected import of common package, please migrate to a responsibility-based package. exit 1 fi通过工具把规则自动化比反复在评审里口头提醒有效得多。当然这个脚本要避免误伤比如项目里可能有 common 是标准库或者第三方合法包名需要自己调整路径前缀。6. 关于 internal/common 和一些真正的例外6.1 internal 限定能把问题锁在内部但并不解决问题很多团队看到官方文档推荐 internal 目录就把 common 挪到internal/common以为这样安全。确实internal 阻止了外部模块引用但它仍然允许内部所有模块引用。对一个几十万行代码的服务端项目来说内部模块之间互相引用 common破坏力并没有减少多少。所以不要把 internal 当免死金牌。但如果你一定要保留一个 common至少应该满足这些条件只放在 internal 下绝不放pkg/文件数量严格控制比如不超过 5 个包内所有函数必须有明确的共同基础而不是一个大杂烩每次往里面加代码前先问“能不能放进具体能力的包”。6.2 真正适合“公共包”的场景能力明确的横切包有一种情况项目里会存在多个底层能力包但不叫 common。比如在商品溯源系统里日志、链路、错误码、响应包装都是横切能力它们天然要被很多业务包依赖。把它们放成internal/logging、internal/trace、internal/errcode、internal/httpreply看起来像是公共包但区别在哪里区别在于这些包每个都只有一个职责你可以独立阅读、独立测试、独立演进。如果有人问你 trace 包里有什么答案是“链路追踪上下文工具”而不是“乱七八糟的公共代码”。这是 common 永远说不出来的话。所以建议团队用“能力包”取代“公共包”的概念命名上就完成职责澄清。6.3 我自己的体会包命名是工程判断力的镜子操作过几次 common 拆除之后我对包命名越来越敏感。去看一个不熟悉的 Go 项目我通常会先打开目录列表如果不借助任何文档光看包名就能勾勒出这个项目的业务结构和依赖层次那这个团队的工程化水平一定不差。反过来如果所有代码都指向某个 common 或者 utils那它的业务边界大概率也已经混乱了。最后分享一个判断技巧当你面对一堆代码不知道该放哪个包时别急着开一个通用包。先把它放到最靠近调用方的地方让真实使用场景告诉你它应该属于哪里。如果后来发现多个模块真要共享再抽到能力包命名自然就对。个人体会是拆 common 不必一蹴而就。先从一个最独立的函数开始让团队感受到好处再一步步推进比一口气大动干戈更容易坚持下来。你的项目可能无法一天内消灭 common但只要这个方向确立了代码评审时大家都会主动问“这个新文件真的要放 common 吗”——能问出这句话比任何规范文档都管用。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号