跳到正文

10 模块、测试与工具链 ​

一句话:Go 的工程能力来自「约定 + 工具链」:module 定义依赖边界,包名和大小写决定可见性,go test/vet/build 把检查固化成命令;把这些用顺,比记住某个框架更重要。

适用 Go 1.22+;module 从 1.11 引入,1.16 起默认开启。

为什么需要 ​

单文件程序不需要工程化;多包、多团队、多版本依赖时,才需要可复现的依赖、明确的可见性边界和统一检查。Go 故意不提供复杂构建系统:go.mod 加目录约定加标准命令,降低协作成本。代价是布局靠约定,没有强制分层,团队要自己定规则并写进 CI。

核心机制 ​

module 与依赖 ​

bash
go mod init example.com/tooling   # 生成 go.mod
go mod tidy                       # 增删依赖,更新 go.sum
go mod download                   # 下载到模块缓存
go mod vendor                     # 复制依赖到 vendor/
go list -m all                    # 当前依赖树
go mod verify                     # 校验缓存

go.mod 记录 module 路径、Go 版本和依赖,go.sum 记录校验和,两者都要提交。go get 修改依赖,go get pkg@version 指定版本,不要手改 go.sum。replace 只对当前主模块生效,适合本地联调和临时 fork,不要把它当发布方案。有 vendor/ 时构建默认使用 vendor(1.14+),CI 要确认它与 go.mod 一致。

包、导出与 internal ​

  • 一个目录一个包;包名短、小写、无下划线,调用方看到的是包名而不是目录名。
  • 首字母大写导出,小写包内可见,没有 private 关键字。
  • internal/ 下的包只能被父目录及以内的模块导入,这是编译器强制的边界,不是安全机制。
  • 避免循环导入:把共享类型放到更底层的小包,或用接口解耦。
  • 常见布局:cmd/<app>/main.go 放入口,根包或 internal/ 放实现;pkg/ 只在确实对外复用时才建,不是标配。

测试 ​

go
func TestAdd(t *testing.T) {
	cases := []struct {
		name string
		a, b int
		want int
	}{
		{"positive", 1, 2, 3},
		{"negative", -1, 2, 1},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := Add(tc.a, tc.b); got != tc.want {
				t.Fatalf("Add(%d,%d)=%d, want %d", tc.a, tc.b, got, tc.want)
			}
		})
	}
}

表驱动把用例和数据分开,t.Run 子测试可以单独跑 -run TestAdd/positive。t.Helper() 让失败行号指向调用处,t.Cleanup 注册清理并按逆序执行。同包测试能访问未导出标识符,_test 包测试只测公开 API。-race 查数据竞争,-cover 看覆盖率,-count=1 禁用缓存,-bench 加 -benchmem 看分配。集成测试用 build tag 或 testing.Short() 隔离。

工具链 ​

bash
gofmt -w .                       # 格式化
go vet ./...                     # 静态检查
go build ./...                   # 编译所有包
go test ./... -race -cover       # 测试
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o demo ./cmd/demo
go tool pprof -http=:8081 http://127.0.0.1:6060/debug/pprof/profile

go vet 查可疑代码(printf 参数、锁复制、结构体 tag),staticcheck 是更全的第三方检查。交叉编译只需改 GOOS/GOARCH;涉及 cgo 时要目标平台工具链,通常设 CGO_ENABLED=0 得到静态二进制。pprof 入口是 import _ "net/http/pprof" 并启动 HTTP server,生产不要暴露公网。

最小上手 ​

bash
cd examples/10_tooling
go test -v ./...                 # 表驱动测试,包含子测试
go test -race ./...              # 数据竞争检查
go test -bench=. -benchmem ./... # 基准测试
go vet ./...
go run ./cmd/demo                # 输出 3
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /tmp/demo ./cmd/demo

示例 module 见 examples/10_tooling,包含 go.mod、calc/ 库包、calc/calc_test.go 和 cmd/demo/main.go。测试代码与上面的表驱动写法一致。

生产动作 ​

  • CI 顺序:gofmt -l 有输出就失败,然后 go vet ./...、go test -race ./...、go build ./...;Go 版本固定到 go.mod 和 CI toolchain。
  • 依赖:定期 go list -m -u all 看升级;go mod tidy 后提交 go.mod/go.sum;vendor 项目在 CI 校验 go mod vendor 后没有 diff。
  • 发布:-trimpath 去掉本地路径,-ldflags "-s -w" 减小体积但会去掉部分调试信息;版本号用 -X 注入变量。
  • 测试:错误分支、边界、并发都要覆盖;race detector 只在测试和 CI 开,生产二进制不要带。
  • 观测:pprof 只在诊断时开,长期指标用 runtime/metrics;不要长期暴露 /debug/pprof。

边界 ​

  • replace 不会传递给依赖你的模块,库作者不要依赖本地 replace。
  • internal 是导入路径规则,不是运行时隔离。
  • pkg/ 不是必需,没有外部复用就不要建。
  • go test 缓存会掩盖环境变化,排障用 -count=1。
  • race detector 有性能开销,且只能发现实际执行到的竞争;它依赖 cgo 和 C 编译器,Windows 上没装 gcc 会直接构建失败。
  • 交叉编译加 cgo 需要目标平台工具链;CGO_ENABLED=0 可能改变 DNS 和证书行为,以官方文档为准。

讲清楚 ​

Go 的工程化不是靠框架,而是靠 module、包边界和标准命令。go.mod 固定 module 路径、Go 版本和依赖,go.sum 校验完整性,go mod tidy 让两者和代码一致;replace 只影响当前主模块。可见性由首字母大小写决定,internal/ 用导入路径强制边界。测试以表驱动为主,t.Run 拆子测试,-race 查并发,benchmark 看分配。工具链把格式、静态检查、测试、构建固化成 CI 命令,交叉编译只改 GOOS/GOARCH。这套约定的代价是布局没有强制标准,需要团队自己定并写进流水线。记住三条:依赖可复现、边界可检查、失败可定位。

适用 Go 1.22+ | 初稿 2026-10

把一门技术讲透到既懂也会用