关键在于模块需有独立go.mod且仅依赖标准库及不可替代底层包,业务逻辑封装为纯函数,依赖通过构造函数注入,http暴露退化为标准http.handler,测试须脱离网络与磁盘运行。

不需要引入 gin、echo 或 fiber,也能写出可复用、可测试、符合 Go 生态习惯的模块——关键不是“不用框架”,而是不依赖外部 HTTP 路由器、中间件链、注入容器这些重量级抽象。
模块边界必须靠 go.mod 显式声明
很多人误以为只要不 import 框架就“纯净”,结果发现模块里悄悄依赖了 github.com/sirupsen/logrus 或 gopkg.in/yaml.v3,这其实已经偏离“纯净”定义。真正的起点是:你的模块必须有独立的 go.mod,且 require 列表只保留语言标准库和真正不可替代的底层依赖(比如 golang.org/x/exp/maps 这类实验性但无替代品的包)。
- 运行
go mod init example.com/mybiz创建模块,不要沿用主项目 module path -
go mod tidy后检查go.sum—— 如果出现非标准库的第三方哈希,立刻定位来源并评估是否真不可替换 - 避免用
//go:embed加载模板或配置文件,除非你明确接受该模块对 Go 1.16+ 的硬性版本约束
HTTP 接口暴露应退化为函数签名 + 标准 http.Handler
不写 router.GET("/user", handler),不注册中间件栈,而是把业务逻辑封装成纯函数,再由调用方决定如何接入 HTTP、gRPC 或 CLI。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 接口定义示例:
func GetUser(ctx context.Context, id string) (*User, error)—— 输入输出清晰,无框架类型污染 - 若需 HTTP 封装,只用标准库:
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }),不引入任何路由逻辑 - 拒绝在模块内做
log.Printf或http.Error—— 错误由上层统一处理,模块只返回error值
配置与依赖注入必须通过构造函数参数传递
没有 config.Load() 全局单例,也没有 service.NewService() 隐式读取环境变量。所有外部依赖(DB、缓存、HTTP 客户端)都显式传入。
- 典型构造函数:
func NewUserService(db *sql.DB, cache *redis.Client) *UserService - 数据库连接不封装成
db.Open(...)调用 —— 上层负责初始化并传入已配置好的*sql.DB - 时间依赖用
clock.Clock接口(如github.com/andres-erbsen/clock),而非直接调用time.Now(),便于单元测试
测试必须能脱离网络和磁盘运行
模块的 go test 应该在 CI 中秒级完成,且不触发真实 API 调用或文件 I/O。这意味着你要提前隔离副作用。
- 所有外部服务(DB、Redis、HTTP Client)都定义 interface,测试时用
gomock或手写 fake 实现 - 避免在
init()函数中加载配置或初始化全局状态 —— 这会让测试无法重置环境 - 使用
testify/assert可以,但别用testify/suite这类带生命周期钩子的扩展,它隐含状态管理
最容易被忽略的一点:模块的 “纯净” 不体现在代码行数少,而在于它的 go list -deps 输出里,除标准库外只有你主动选择的、有明确用途的极少数包 —— 多一个 github.com/pkg/errors 都算破功,除非你真需要它的堆栈增强能力且已评估过 fmt.Errorf("...: %w", err) 不够用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










