go项目应按职责边界而非类型堆砌拆分pkg子包,如pkg/strutil仅处理字符串安全操作,pkg/httpx封装http客户端配置,各包禁止依赖业务代码、不读全局状态、导出函数须显式传参,版本破坏性变更必须路径升级。

别建 pkg/utils 或 internal/common —— 这类包会在三个月内变成无人敢动的“上帝包”,且无法做细粒度版本控制或独立测试。
按职责边界拆分 pkg/ 子包,而不是按类型堆函数
把字符串处理、HTTP 客户端、错误包装塞进同一个包,等于把螺丝刀、电钻和焊枪装进一个工具箱——用的时候全得翻一遍。Go 项目里真正可维护的公共包,是按“它解决什么问题”来命名的,不是按“它操作什么类型”。
-
pkg/strutil只做字符串安全截断、正则提取、模糊匹配,不碰 slice 或 error -
pkg/httpx封装http.Client实例创建、超时、重试、trace 注入,不封装具体业务 API 调用逻辑 -
pkg/errx提供Wrap、Is、As和预定义错误码(如ErrNotFound),不依赖任何业务 model 或 service -
pkg/log暴露Logger接口和带 request_id 的字段注入,不暴露*zap.Logger实例,也不调用os.Getenv
每个子包只 import 标准库或明确声明的第三方(如 go.uber.org/zap),CI 中可用 go list -f '{{.Imports}}' pkg/log | grep -E '(internal|domain)' 自动拦截业务污染。
导出函数必须显式传参,拒绝全局状态和隐式配置
写 func SendEmail(to string) 看似简单,实际绑死了 SMTP 地址、超时、认证方式——测试要 patch 全局变量,线上无法为不同场景设不同 timeout。正确做法是让调用方掌控上下文。
- 基础函数只接收必要参数:
ParseJSON(data []byte) (map[string]interface{}, error) - 可选行为用函数选项模式:
WithTimeout(30*time.Second)、WithRetry(3) - 绝不读环境变量、不读全局 struct、不调用
init()初始化配置 - 返回
error,不 panic(启动期配置校验失败除外)
例如 pkg/httpx.NewClient(opts ...ClientOption) 内部默认 timeout 是 5s,但调用方可以自由覆盖:NewClient(WithTimeout(10*time.Second), WithRetry(2))。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
测试辅助函数必须带 t.Helper(),且仅限 _test.go 内部或 pkg/testutil
把 assertStatus、newTestDB、setupTempDir 放进业务包或通用工具包,会导致非测试代码意外依赖 testing.T,编译失败。测试逻辑和运行时逻辑必须物理隔离。
- 单元测试中重复出现的断言、数据构造、资源清理,应封装成以
t *testing.T为第一参数的函数 - 函数内部第一行必须是
t.Helper(),否则报错行号指向辅助函数内部而非测试用例 - 临时资源(如文件、端口、数据库连接)要用
t.Cleanup()注册释放,避免泄漏 - 若多个包共用同一套测试辅助逻辑,单独建
pkg/testutil,但禁止被非测试代码 import
示例:func assertEqual(t *testing.T, expected, actual interface{}) { t.Helper(); if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %+v, got %+v", expected, actual) } }。
版本升级必须路径变更,禁止 v2 包复用 v1 导入路径
当你改了 pkg/strutil.Trim 的签名,或删了某个选项函数,就不是“小更新”——它是破坏性变更。Go 的模块机制要求你用新路径隔离,否则下游项目 go get 后直接 panic。
- v1 版本路径:
github.com/yourorg/strutil - v2 不兼容变更后路径:
github.com/yourorg/strutil/v2 - 发布前运行
go list -m all检查是否意外引入 heavy 依赖(如golang.org/x/tools) - README 明确标注支持 Go 版本(如 “Go 1.21+”),并给出迁移示例
最常被忽略的是:哪怕只是给一个函数加了个新选项参数,只要旧调用方式不再能编译,就得升 v2 并换路径——Go 没有“向后兼容的不兼容变更”这种东西。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










