适用读者:要在一个团队里落地 Go 规范的人;要决定"用 Gin 还是标准库"的人。
前置:S09-J04/J05、A01(模块与依赖)、A02(选型 ADR)、S06(工程化)。
覆盖旧文:3627(Go 编码规范)、3658(Go 语言单测)、3613(依赖管理)、3628(环境搭建)、3629(Gin)、3630(GORM)、3657(Go 语言前景)→ 7 篇 / 约 10 万字符。
扩展点:E-S09-019(规范的"可自动化"部分 vs "靠评审"部分)、E-S09-020(表驱动单测 +-race的最小可用流水线)、E-S09-021(Gin/GORM 的选型边界)。
下一篇:S09-X02 · 教程章节与题库汇总登记。
1. 一句话结论
规范能否活下来,取决于它有多少能被工具强制执行。 命名、错误包装、包划分可以进 linter;"包要小而内聚""不要滥用泛型"只能靠评审。先把可自动化的那部分做成 CI 红线,剩下的写进评审清单。
出处:
A-3627(编码规范)、A-3658(单测)、A-3613(依赖)、A-3629/A-3630(框架)。
2. 规范:可自动化 vs 靠评审(E-S09-019)
| 规范项 | 能否自动检查 | 工具 |
|---|---|---|
| 格式(缩进/换行/import 分组) | ✅ | gofmt、goimports |
命名(导出 MixedCaps、包名小写无下划线) |
✅(部分) | revive、staticcheck |
错误包装用 %w、禁裸 panic |
✅ | errcheck、revive 规则 |
| 未使用的变量/参数 | ✅ | 编译器 + unparam |
| 复杂度过高 | ✅(阈值) | gocyclo |
| 包要小而内聚 | ❌ | 评审 |
| 接口应由使用方定义 | ❌ | 评审 |
| 不要为通用而泛型化 | ❌ | 评审 |
一页规范(可直接抄):
// 包名:小写单词,不用下划线/复数(http 不是 https,user 不是 users)
// 导出:MixedCaps;未导出:mixedCaps;缩写全大写或全小写(URL、ID → url、id 视上下文)
// 错误:%w 包装保留链;判断用 errors.Is/As;不用 errors.New 拼动态串(用 fmt.Errorf)
// 接口:定义在使用方包内,方法数 1~3;不要预先抽象
// 并发:goroutine 的退出路径必须显式(ctx / close(ch));不要把 ctx 存进结构体
// 结构体:字段顺序按"重要度";导出结构体加零值可用性说明
// 错误处理的标准三段式(Go 1.20+)
if err := do(); err != nil {
return fmt.Errorf("load config: %w", err) // ① 包装带上下文
}
var target *NotFoundError
if errors.As(err, &target) { ... } // ② 类型提取
if errors.Is(err, os.ErrNotExist) { ... } // ③ 哨兵比较
判据:"规范文档超过两页就没人读"——把两页写进 CONTRIBUTING.md,其余交给 linter 配置。
3. 单测:表驱动 + 竞态 + 覆盖率(E-S09-020)
func TestParseDuration(t *testing.T) {
cases := []struct {
name string
in string
want time.Duration
err bool
}{
{"秒", "30s", 30 * time.Second, false},
{"非法", "abc", 0, true},
{"空串", "", 0, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := ParseDuration(c.in)
if (err != nil) != c.err { t.Fatalf("err=%v want err=%v", err, c.err) }
if got != c.want { t.Errorf("got=%v want=%v", got, c.want) }
})
}
}
| 项 | 结论 |
|---|---|
表驱动 + t.Run |
用例可并行、失败定位到子测试名 |
| 竞态 | go test -race ./...(CI 必跑;本地慢可只跑关键包) |
| 覆盖率 | -cover 看行覆盖;不要为凑覆盖率写无断言测试 |
| HTTP 测试 | httptest.NewServer / httptest.NewRecorder,不起真实端口 |
| 基准 | BenchmarkXxx + -benchmem 看 allocs/op |
testify 取舍 |
assert 可读性好,但 require 才能在失败时停止;标准库 + 少量辅助函数已够 |
| 分层 | 单测(无 IO)→ 集成测(-tags=integration)→ E2E,分开跑 |
最小 CI 流水线:
gofmt -l . | tee /tmp/fmt; test ! -s /tmp/fmt # ① 格式
go vet ./... # ② 静态检查
staticcheck ./... # ③ 深度检查(可选)
go test -race -coverprofile=cover.out ./... # ④ 竞态 + 覆盖
go build ./... # ⑤ 构建
判据:"先跑 -race 再谈并发正确性"——很多"偶发 panic"在 -race 下是稳定复现的。
4. 依赖管理:MVS 与可复现构建
go.mod:module + go 版本 + require(直接与间接依赖)
go.sum:校验和(防篡改)
MVS(最小版本选择):取满足所有约束的最低版本(不取最新)
| 场景 | 做法 |
|---|---|
| 升级依赖 | go get [email protected] / go get -u ./...(谨慎,可能连锁升级) |
| 查为什么引入 | go mod why pkg、`go mod graph |
| 替换本地/分支 | replace old => ../local(不要提交到主干) |
| 离线构建 | go mod vendor + -mod=vendor |
| 私有模块 | GOPRIVATE + GONOSUMDB 配置 |
| 可复现 | 提交 go.mod/go.sum;CI 用 -mod=readonly |
坑 1:replace 留在主干会让别人构建失败——只在本地排查时用。
5. 框架选型:Gin vs 标准库,GORM vs sqlc/sqlx(E-S09-021)
| 维度 | 标准库 net/http |
Gin | 结论 |
|---|---|---|---|
| 路由 | Go 1.22 起支持方法+路径参数(GET /user/{id}) |
成熟的路由树 + 分组 | 简单 API 用标准库已够 |
| 中间件 | 手写 func(http.Handler) http.Handler |
Use() 链式 |
Gin 更方便,标准库更透明 |
| 绑定/校验 | 手写 json.Decode |
ShouldBind + validator |
Gin 省事,但错误信息需统一 |
| 性能 | 相当(Gin 优势主要在旧版本基准) | 略优 | 不构成选型主因 |
| 生态 | 零依赖 | 大量中间件 | 团队熟悉度优先 |
| 维度 | GORM | sqlc | sqlx |
|---|---|---|---|
| 写法 | 链式 API / 结构体映射 | 写 SQL → 生成类型安全代码 | 手写 SQL + 结构体扫描 |
| 复杂查询 | 易失控(N+1、隐式 join) | SQL 可控 | SQL 可控 |
| 迁移 | AutoMigrate(生产慎用) | 配 goose/atlas | 同 |
| 学习成本 | 低 | 中(需写 SQL) | 中 |
| 适用 | CRUD 为主、快速开发 | SQL 复杂、要性能可控 | 中等复杂度 |
判据:"能写 SQL 的团队用 sqlc/sqlx,写不动 SQL 的团队用 GORM,并接受它生成的查询需要 review"——争议不在工具,在团队对 SQL 的掌控程度。
6. Go 的适用边界(3657 的结论重写)
| 场景 | 适合 | 理由 |
|---|---|---|
| 网络服务 / API 网关 | ✅ | 高并发、部署简单(单二进制) |
| CLI 工具 / DevOps | ✅ | 交叉编译、启动快 |
| 云原生基础设施 | ✅ | k8s/Docker 生态原生语言 |
| 数据密集计算(矩阵/科学计算) | ❌ | 缺成熟数值库,性能不如 Fortran/C++ |
| 富客户端 GUI | ❌ | 生态薄弱 |
| 超大单体业务系统 | ⚠️ | 需要团队规范与分层纪律(否则 interface 泛滥) |
7. 反例:五种"规范写了但没落地"
| 做法 | 结果 |
|---|---|
| 规范只有文档,CI 不检查 | 三个月后格式五花八门 |
用 golangci-lint 全开 100+ 规则 |
满屏警告 → 全部忽略 |
| 覆盖率当 KPI | 写出大量无断言测试 |
AutoMigrate 上生产 |
隐式改表 + 无回滚 |
| 为"将来可能换实现"预抽接口 | 接口泛滥、阅读成本上升 |
8. 自测
- 规范里哪些能自动化?哪些只能评审?
- 表驱动单测的三要素?
-race为什么必跑? - MVS 是什么?
replace为什么不能提交? - Gin 与标准库的选型判据?GORM 与 sqlc 的分界?
- 五种"规范没落地"的典型?
答案
- 格式/命名/错误包装/复杂度可自动化;内聚、接口定义位置、泛型克制靠评审。
cases切片 +t.Run+ 明确断言;竞态检测能把偶发问题稳定复现。- 最小版本选择;会让他人构建依赖本地路径失败。
- 团队熟悉度与路由/绑定需求;SQL 掌控力决定 GORM vs sqlc。
- 无 CI 检查、规则全开、覆盖率 KPI、AutoMigrate 上生产、预抽接口。
9. 下一篇
S09-X02 · 教程章节与题库汇总登记 —— 18 篇语言教程章节与现有 B02–B07 的逐条比对结论、7 篇题库汇总的去向、以及 Go 学习路线的最终形态。
本篇属于系列《S09 · Go 语言》。旧文来源:A-3627、A-3658、A-3613、A-3629、A-3630、A-3628、A-3657;扩展点 E-S09-019/020/021。