从 gonamefix 到 Go code-style:lint、skill 和人工判断的边界
Jun 9, 2026 - ⧖ 3 minTLDR
gonamefix 这个名字很容易让人误会成“要不要继续写一个 Go 命名 linter”。但真正的问题不是这个。
我原来想做的是:系统理解 Go code-style 最佳实践,把能被机器稳定检查的部分收进 golangci-lint,把不能稳定检查但值得反复提醒的部分沉淀成 skill/manual review,最后形成一套能降低心智负担的工程习惯。
所以 M07 的结论不应该是“现在不做独立 gonamefix”这么窄。更准确的结论是:gonamefix 是一次把问题理解窄了的失败尝试;真正应该 close 的是 Go code-style 治理闭环。
1. 这个待办为什么被误解
data/gh/langs/golang.yml 里那段注释原本长这样:
# MAYBE: [2025-09-11] gonamefix. 以后有时间的时候再写,其实用不太到
# 以下这些已经整理到 golangci-lint.yml 的rules了,所以仅供参考
# [TOP 20 Go最佳实践](https://colobu.com/2023/11/17/golang-quick-reference-top-20-best-coding-practices/)
# [不要写破坏性的 Go 库](https://mp.weixin.qq.com/s?__biz=...)
# [Go中的一些优化笔记,简单而不简单](https://mp.weixin.qq.com/s/X8c6ZIJdBFptYA9CRj6wnA)
# [[长文]从《100 Go Mistakes》我总结了什么?](https://www.luozhiyun.com/archives/797)
# [深度阅读之《100 Go Mistakes and How to Avoid Them》](https://qcrao.com/post/100-go-mistakes-reading-notes/)
# [Chinese Version - 100 Go Mistakes and How to Avoid Them](https://100go.co/zh/)
# [Tencent/secguide Go安全指南](https://github.com/Tencent/secguide/blob/main/Go安全指南.md)
如果只盯着第一行,确实会得出一个很浅的判断:现有 golangci-lint、revive、staticcheck、go vet 已经覆盖很多命名和风格检查,所以不用再做独立 gonamefix。
这个判断不是完全错,但它错在把任务缩小了。
这段注释旁边真正重要的是那些资料源:Go 最佳实践、不要写破坏性库、性能优化、100 Go Mistakes、Go 安全指南。它们指向的不是“变量名太长怎么办”,而是一个更大的问题:
Go 代码到底应该怎样写,哪些习惯能靠工具强制,哪些只能靠理解和审查?
这才是 M07 应该处理的对象。
2. gonamefix 失败在哪里
xbpk3t/gonamefix 这次尝试的方向是:基于 go/analysis 写一个 linter,扫描标识符,把一些长词替换成短词,比如:
| 原词 | 建议 |
|---|---|
request |
req |
response |
res |
parameter |
param |
database |
db |
configuration |
config |
context |
ctx |
从工程实现角度看,这不算离谱。go/analysis 是写 Go linter 的正确基础,custom golangci-lint 插件也可以打包进去。真正的问题在规则本身。
Go 命名不是“越短越好”。Go 的命名原则更接近:
名字长度应该和作用域、上下文、公共 API 稳定性成比例。
req 在 HTTP handler 的三行局部代码里很好;Request 作为一个导出的业务类型也可能很好。config 很常见;但把所有 configuration 都机械改成 config,未必让代码更清楚。user 改成 usr 更危险,因为 Go 社区并没有普遍接受 usr 作为比 user 更好的名字。
这类规则一旦做成 auto-fix,还会有几个硬问题:
| 问题 | 为什么严重 |
|---|---|
| 语义不稳定 | 同一个词在局部变量、导出类型、接口、字段里含义不同 |
| API 破坏 | 导出符号机械 rename 会直接破坏调用方 |
| 个人偏好过重 | req/res/ctx/db 是惯例,usr/val/cnt 不一定是 |
| 跨包 rename 难 | 真要安全改名,不能只替换 AST ident,还要考虑引用、生成代码、测试和外部 API |
| 与现有工具重叠 | initialism、receiver、underscore、predeclared shadow 等已有成熟 linter |
所以 gonamefix 的失败不在于“不会写 linter”,而在于把一个需要判断的问题做成了机械替换问题。
未来如果还要保留自定义 linter 的空间,它应该非常小:项目专属 acronym、禁止明显垃圾包名、固定 import alias、少数公共 API 命名约定。没有这些稳定规则前,自定义 linter 维护成本大于收益。
3. lint 之前,先确认配置真的生效
这次复盘里最有价值的发现不是某条规则,而是执行链路本身有问题。
以 docs-alfred 为例,主配置在:
/Users/luck/Desktop/docs-alfred/.github/linters/golangci-lint.yml
修复前,Taskfile.yml 里的入口是:
lint :
cmds :
- golangci-lint run --fix
- gofumpt -l -w .
- go mod tidy -v
- task : pre-commit
- nilaway ./...
这个命令没有显式 -c .github/linters/golangci-lint.yml。实际检查时:
golangci-lint config path
返回的是:
No config file detected
这意味着一个很严重的事实:平时跑 task lint 时,很可能根本没有吃到那份以为已经整理好的配置。
这类问题的修复不复杂,但必须明确:task lint 也要和 pre-commit 一样显式加载同一份配置。
lint :
cmds :
- golangci-lint run --fix -c .github/linters/golangci-lint.yml
再跑本机版本:
golangci-lint --version
结果是:
golangci-lint has version 2.12.2
然后验证配置本身:
golangci-lint config verify -c .github/linters/golangci-lint.yml
这一步必须在本机当前版本下通过。复盘过程中暴露过的典型 schema 问题包括:
| 问题 | 含义 |
|---|---|
version 是 number |
v2 schema 需要 string |
issues.exclude-* |
旧字段位置已经不被 v2 接受 |
testpackage.skip-imports |
当前 schema 下不是合法字段 |
revive.rules[].message |
revive rule 配置里这个字段不合法 |
wrapcheck.ignoreSigs |
字段名/位置不符合当前版本 schema |
pre-commit 里的 golangci-lint-config-verify 也必须能实际运行,不能因为文件过滤变成:
golangci-lint-config-verify..........................(no files to check)Skipped
现在这条 hook 应该明确通过:
golangci-lint-config-verify..............................................Passed
这就是第一条原则:
不要先讨论规则哲学。先证明配置文件被同一套入口加载,并且能被当前版本校验通过。
否则所谓“已经整理到 golangci-lint.yml 的 rules”只是心理安慰。
4. Go code-style 的分层
Go code-style 不是一层东西。把所有规则都塞给 golangci-lint,最后一定会变成误报、噪声和维护负担。
更合理的分层是:
| 层级 | 代表工具/机制 | 适合处理什么 |
|---|---|---|
| formatter | gofmt、gofumpt、goimports、gci |
空白、import、语法格式 |
| compiler/typecheck | go test、go test ./... |
类型、构建、测试正确性 |
| official analysis | go vet、govet |
copylocks、printf、lostcancel 等高置信问题 |
| static analysis | staticcheck、unused、ineffassign |
bug、简化、不可达、无效赋值 |
| focused linters | errcheck、bodyclose、errorlint、gosec |
错误处理、资源关闭、安全、HTTP/SQL 常见坑 |
| style linters | revive、gocritic、misspell、varnamelen |
命名、复杂度、风格和部分性能建议 |
| manual review | code review / skill | API 设计、抽象边界、兼容性、并发结构、性能取舍 |
| writing | blog / notes | 解释为什么,不让规则变成迷信 |
golangci-lint 应该负责中间几层,不应该试图吞掉所有层。
5. 资料源和 lint 覆盖关系
下面不是逐字摘要,而是把这些资料中的实践主题映射到工具覆盖边界。
| 资料源 | 主要主题 | lint 可覆盖 | lint 覆盖不了 |
|---|---|---|---|
| TOP 20 Go 最佳实践 | 错误处理、slice/map、并发、context、复杂度、命名 | errcheck、errorlint、gocritic、gocyclo、gocognit、bodyclose、contextcheck |
是否该抽象、API 是否好用、错误边界是否合理 |
| 不要写破坏性的 Go 库 | 公共 API 稳定性、兼容性、行为变更 | 少量靠 revive exported、测试和 API diff 辅助 |
语义版本、兼容承诺、行为是否破坏调用方 |
| Go 优化笔记 | 分配、字符串、slice、逃逸、benchmark | gocritic、prealloc、perfsprint、govet fieldalignment、ineffassign |
是否真的需要优化、瓶颈是否被 profile 证明 |
| 100 Go Mistakes | 数据类型、控制流、错误、并发、context、测试、性能 | govet、staticcheck、copyloopvar、bodyclose、rowserrcheck、sqlclosecheck、errorlint、gosec |
架构判断、测试策略、性能模型、API 设计 |
| qcrao / luozhiyun 阅读笔记 | 对 100 mistakes 的二次整理 | 可作为索引,映射到上面相同规则 | 二手摘要不能替代原始条目判断 |
| 100go.co/zh | 原书条目的中文入口 | 适合作为 coverage matrix 的主索引 | 不能把每条都变成 linter |
| Tencent Go 安全指南 | 输入校验、命令执行、文件路径、SQL、SSRF、crypto、敏感信息 | gosec、govet、errcheck、noctx、bodyclose、loggercheck |
权限模型、业务鉴权、威胁建模、部署密钥管理 |
| cxuu/golang-skills | 用 skill 承载 Go 代码审查经验 | 不属于 lint 覆盖 | 命名、接口、包边界、并发和错误处理的判断流程 |
这个表说明了一件事:golangci-lint 很强,但它不是 Go 最佳实践本身。它只是把一部分高置信规则自动化。
6. golangci-lint 应该怎么取舍
我更倾向于“实用严格”,不是 default: all。
default: all 适合探索:先看看生态里有哪些规则,会报哪些问题。但它不适合作为长期治理状态。原因很简单:
- 新版本会新增 linter,行为会漂移。
- deprecated linter 会制造维护噪声。
- opinionated 规则会压过真实问题。
- 大量 exclude 会让配置越来越像垃圾场。
更稳的方式是把规则分层启用。
6.1 应该稳定保留的核心规则
这些规则更接近 bug detector,而不是风格偏好:
| 类别 | 建议规则 |
|---|---|
| 基础正确性 | errcheck、govet、staticcheck、unused、ineffassign |
| 错误处理 | errorlint、errname、nilerr、nilnil |
| 资源关闭 | bodyclose、rowserrcheck、sqlclosecheck |
| 并发/context | copyloopvar、contextcheck、containedctx、fatcontext |
| 安全 | gosec、bidichk |
| 日志/可观测 | loggercheck、sloglint,如果项目使用 slog |
| 简化和现代化 | unconvert、usestdlibvars、modernize,需要跟 Go 版本配合 |
这些规则的共同点是:它们报出来的东西通常能解释成“这里可能真有 bug”。
6.2 可以保留,但必须调阈值的规则
这些规则有价值,但不能让默认值主导项目:
| 规则 | 建议态度 |
|---|---|
gocritic |
启用 selected tags/checks,不要无脑全开 experimental/opinionated |
revive |
只保留明确有用的命名、receiver、error-flow、comment 规则 |
gocognit / gocyclo |
用宽松阈值抓明显复杂函数,不拿它做教条 |
funlen |
可以对测试放宽;数据转换类函数不要误伤太多 |
dupl |
适合提醒,不适合强制所有重复都抽象 |
goconst |
魔法字符串有用,但测试和小常量要放宽 |
varnamelen |
可以训练习惯,但要允许 err、ctx、id、i 等惯例 |
lll |
对 generated、URL、表格和注释要有例外 |
这些规则的价值在“提醒”,不是“裁判”。
6.3 不适合默认强开的规则
这些规则不是没用,而是容易把个人项目拖进噪声:
| 规则 | 问题 |
|---|---|
wrapcheck |
对 CLI 和 glue code 经常过严,错误边界需要人工判断 |
err113 |
禁止动态 error 很容易干扰轻量代码 |
exhaustruct |
对外部 struct、测试 fixture 和配置结构体噪声大 |
exhaustive |
适合 iota enum,但不是所有 switch 都需要穷尽 |
paralleltest / tparallel |
测试并行不是通用收益 |
testpackage |
黑盒测试很好,但白盒测试也常常合理 |
ireturn |
“接受接口,返回具体类型”是原则,不是机械规则 |
gochecknoglobals |
Go 项目里 sentinel error、regexp、config default、sync.Once 很常见 |
mnd |
魔法数字检测很容易把业务常量和测试数据全打爆 |
wsl / wsl_v5 |
空行风格不值得成为主要治理成本 |
这就是“实用严格”:高置信问题严格,低置信偏好克制。
7. 哪些东西应该做成 skill
Skill 不应该复制 golangci-lint。如果一个问题能被 linter 高置信检查,那就让 linter 做。
Skill 适合承载这些东西:
| 主题 | 为什么适合 skill |
|---|---|
| 命名 | 需要结合作用域、包名、导出性、调用上下文判断 |
| package/API 设计 | 是否 stutter、是否暴露过多、是否破坏兼容性,无法只靠 AST 判断 |
| interface 设计 | 接口放 consumer 侧还是 producer 侧、是否 interface pollution,需要语义判断 |
| error boundary | 哪里 wrap、哪里 sentinel、哪里直接返回,取决于调用边界 |
| context 传播 | linter 能抓缺失 context,但抓不住取消语义是否合理 |
| 并发结构 | goroutine 生命周期、backpressure、errgroup/conc/pool 选择需要设计判断 |
| 性能优化 | 没有 profile 的优化不应该靠规则驱动 |
| 安全审查 | SSRF、鉴权、路径穿越、secret 生命周期需要业务上下文 |
一个合理的 Go code-style skill 应该长这样:
go-code-style-review/
SKILL.md
references/
naming.md
api-boundary.md
errors.md
context-concurrency.md
security.md
performance.md
SKILL.md 只放流程:先读项目 lint 配置和 Go 版本,再跑或引用 lint/test 结果,然后按命名、API、错误、context、并发、测试、安全、性能逐层 review。细节放 references,避免每次把整本 Go style guide 塞进上下文。
关键约束也很重要:
- skill 不能声称替代
golangci-lint。 - skill 不能在没读项目配置前套固定规则。
- skill 输出应该以 review findings 为主,不是泛泛列最佳实践。
- skill 只在 lint 覆盖不到或需要判断时介入。
这比继续堆一个 gonamefix 更接近原始目标。
8. M07 应该怎样 close
M07 不能只写成:
现有 lint 生态够了,不做独立 gonamefix。
这句话太窄,会把真正的问题再次丢掉。
更准确的 close 方式应该是:
gonamefix 是失败尝试。失败原因不是 go/analysis 技术路线错,而是把 Go code-style 的判断问题缩成了机械命名替换问题。
后续不做独立 gonamefix。真正要维护的是三层产物:
1. golangci-lint:承载高置信、可自动化的规则。
2. skill/manual review:承载命名、API、错误边界、context、并发、安全和性能取舍。
3. blog/notes:解释规则为什么存在,避免再次把工具当成目的。
工程上还有几个明确动作:
| 动作 | 目标 |
|---|---|
统一 docs-alfred lint 入口 |
Taskfile、pre-commit、本地命令都显式加载同一份 config |
修到 golangci-lint config verify 通过 |
先保证配置是当前版本可接受的 |
从 default: all 收敛到实用严格 |
避免版本漂移和 opinionated 噪声 |
| 维护 coverage matrix | 每条最佳实践知道由 lint、skill 还是人工承担 |
| 只设计 skill,不急着创建 | 先证明哪些判断真的无法 lint 化 |
9. 最终结论
Go code-style 不是靠背规则解决的,也不是靠一个超大 golangci-lint.yml 解决的,更不是靠 gonamefix 这种机械缩写器解决的。
真正可维护的路径是:
formatter 固定格式
lint 抓高置信问题
test 证明行为
skill 提醒人工判断
blog 解释为什么
gonamefix 这次失败的价值就在这里:它暴露了一个常见冲动——看到一个模糊的代码习惯问题,就想写一个工具把它强制掉。但 Go 的很多好习惯不是“把某个词替换成另一个词”,而是知道什么时候短、什么时候长,什么时候抽象、什么时候重复,什么时候包一层、什么时候直接返回。
所以 M07 的最终结论是:不继续独立推进 gonamefix;把它转化为 Go code-style 治理闭环。golangci-lint 负责能自动化的部分,skill 负责需要判断的部分,blog 负责把这次踩坑留下来,避免下次又把目标理解窄。