从 gonamefix 到 Go code-style:lint、skill 和人工判断的边界

TLDR

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-lintrevivestaticcheckgo 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 gofmtgofumptgoimportsgci 空白、import、语法格式
compiler/typecheck go testgo test ./... 类型、构建、测试正确性
official analysis go vetgovet copylocks、printf、lostcancel 等高置信问题
static analysis staticcheckunusedineffassign bug、简化、不可达、无效赋值
focused linters errcheckbodycloseerrorlintgosec 错误处理、资源关闭、安全、HTTP/SQL 常见坑
style linters revivegocriticmisspellvarnamelen 命名、复杂度、风格和部分性能建议
manual review code review / skill API 设计、抽象边界、兼容性、并发结构、性能取舍
writing blog / notes 解释为什么,不让规则变成迷信

golangci-lint 应该负责中间几层,不应该试图吞掉所有层。

5. 资料源和 lint 覆盖关系

下面不是逐字摘要,而是把这些资料中的实践主题映射到工具覆盖边界。

资料源 主要主题 lint 可覆盖 lint 覆盖不了
TOP 20 Go 最佳实践 错误处理、slice/map、并发、context、复杂度、命名 errcheckerrorlintgocriticgocyclogocognitbodyclosecontextcheck 是否该抽象、API 是否好用、错误边界是否合理
不要写破坏性的 Go 库 公共 API 稳定性、兼容性、行为变更 少量靠 revive exported、测试和 API diff 辅助 语义版本、兼容承诺、行为是否破坏调用方
Go 优化笔记 分配、字符串、slice、逃逸、benchmark gocriticpreallocperfsprintgovet fieldalignmentineffassign 是否真的需要优化、瓶颈是否被 profile 证明
100 Go Mistakes 数据类型、控制流、错误、并发、context、测试、性能 govetstaticcheckcopyloopvarbodycloserowserrchecksqlclosecheckerrorlintgosec 架构判断、测试策略、性能模型、API 设计
qcrao / luozhiyun 阅读笔记 对 100 mistakes 的二次整理 可作为索引,映射到上面相同规则 二手摘要不能替代原始条目判断
100go.co/zh 原书条目的中文入口 适合作为 coverage matrix 的主索引 不能把每条都变成 linter
Tencent Go 安全指南 输入校验、命令执行、文件路径、SQL、SSRF、crypto、敏感信息 gosecgoveterrchecknoctxbodycloseloggercheck 权限模型、业务鉴权、威胁建模、部署密钥管理
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,而不是风格偏好:

类别 建议规则
基础正确性 errcheckgovetstaticcheckunusedineffassign
错误处理 errorlinterrnamenilerrnilnil
资源关闭 bodycloserowserrchecksqlclosecheck
并发/context copyloopvarcontextcheckcontainedctxfatcontext
安全 gosecbidichk
日志/可观测 loggerchecksloglint,如果项目使用 slog
简化和现代化 unconvertusestdlibvarsmodernize,需要跟 Go 版本配合

这些规则的共同点是:它们报出来的东西通常能解释成“这里可能真有 bug”。

6.2 可以保留,但必须调阈值的规则

这些规则有价值,但不能让默认值主导项目:

规则 建议态度
gocritic 启用 selected tags/checks,不要无脑全开 experimental/opinionated
revive 只保留明确有用的命名、receiver、error-flow、comment 规则
gocognit / gocyclo 用宽松阈值抓明显复杂函数,不拿它做教条
funlen 可以对测试放宽;数据转换类函数不要误伤太多
dupl 适合提醒,不适合强制所有重复都抽象
goconst 魔法字符串有用,但测试和小常量要放宽
varnamelen 可以训练习惯,但要允许 errctxidi 等惯例
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 入口 Taskfilepre-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 负责把这次踩坑留下来,避免下次又把目标理解窄。