1. 为什么我会把 lint 当成 Go 项目的底线
我先说个真实经历。前几年接手一个老服务,接手的时候代码库已经有几万行,前人留下的风格五花八门,错误处理时有时无,连基本的 go fmt 都没跑过。第一周改需求,我加了一行代码,返回值忘记检查,结果上线后某个接口在特定条件下直接 panic,查日志查了两个小时才定位到是一处 resp.Body.Close() 写漏了。
那是我第一次意识到,Go 语言本身的严谨并不能保证业务代码严谨,人的注意力终归有限。从那之后,我给自己定了一条规矩:不管是个人项目还是团队项目,第一件事不是写功能,而是先把 lint 体系搭起来。
我说的 lint,不是某一个小工具,而是围绕 Go 代码的一整套静态检查体系。它的作用是在代码真正跑起来之前,把肉眼看不见的风险挑出来:漏掉的错误处理、不安全的类型断言、可疑的 defer 使用、无用的代码分支、未关闭的资源,等等。它不会帮你写代码,但它能拦住很大一部分“看起来没问题、跑起来就出事”的代码。
这篇文章面向的读者,是那些已经在写 Go、但还没认真搭建过 lint 体系的开发者。如果你刚接触 Go,也建议先看,因为你从现在开始养成习惯,远比你写了几万行之后再回头补要省力得多。我会从工具选型讲到配置细节,从本地工作流讲到 CI 集成,最后把我踩过的坑全部列出来,争取让你看完之后能直接照着搭一套。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具选型:go vet 为什么不够,golangci-lint 为什么能打
2.1 先看 Go 自带的检查工具在哪里
Go 工具链里其实自带了一个 vet 工具,很多人会问:既然官方有了,为什么还要再引入一套 lint 体系?
go vet 做的是基础检查,覆盖面有限。它能发现一些明显的错误,比如 Printf 的参数不对、结构体 tag 格式有问题、copylocks 复制了锁、unreachable 代码等。这些检查很好,但它的定位是“编译期之外的基础体检”,不是“代码质量门禁”。它不会告诉你某个 defer 里的错误被吞掉了,不会检查 Close() 的返回值,也不会对你的代码风格做任何约束。
我见过很多项目的 Makefile 里只写了一句 go vet ./...,然后就觉得质量有保障了。实际上,go vet 在很多关键场景下是放行的。举个例子:
go复制f, _ := os.Open("data.txt")
defer f.Close()
这段代码 go vet 不会报任何问题,但这里存在两个隐患:Open 的错误没有处理,Close 的错误也没有处理。文件打开失败时 f 是 nil,调用 f.Close() 会直接 panic。这种代码如果跑在关键路径上,就是定时炸弹。
所以我的观点是:go vet 不能不用,但也不能只用它。真正要建立质量防线,需要一个能聚合多个检查器的工具,于是我选择了 golangci-lint。
2.2 选择 golangci-lint 的理由
golangci-lint 不是单一检查器,它是一个聚合框架。它能调用几十种 linter,同时支持并行执行,把结果统一格式输出。我选它的原因有三个。
第一,覆盖面广。它内置了 linters 集合,从常见的 errcheck、govet、staticcheck,到风格类的 gofmt、goimports、revive,再到安全类的 gosec,基本覆盖了日常开发的所有检查需求。不用自己一个个去装、去配,一个二进制搞定。
第二,并行执行效率高。golangci-lint 会把代码分析拆成多个任务并行跑,在大型项目上比逐个执行 linter 快得多。我维护的一个服务有近三百个 Go 文件,全量跑一遍 lint 大概在三十秒左右,如果拆成单个 linter 单独跑,可能需要两分钟以上。
第三,配置灵活。你可以通过 .golangci.yml 文件精确控制启用哪些 linter、禁用哪些 linter、每个 linter 的细节规则如何设置。还可以针对某些文件、某些行用 //nolint 注释豁免,这种细粒度控制在实际开发中非常实用。
我再提一句,golangci-lint 的社区活跃度很高,新的 Go 版本发布后它跟进很快。比如 Go 1.18 引入泛型之后,很多 linter 对泛型代码的支持是我比较过所有工具里跟进最快的。
2.3 常见工具组合对比
我整理了一张表,把我在实践中用过的几种方案列出来,方便你根据自己的场景选择:
| 方案 | 覆盖范围 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 只用 go vet | 基础错误 | 内置、零成本 | 覆盖面窄,无风格检查 | 练手、超小工具 |
| go vet + gofmt | 基础错误 + 格式 | 命令简单 | 仍然缺少错误处理和资源检查 | 小型个人项目 |
| go vet + staticcheck | 基础错误 + 静态分析 | 不需要额外框架 | 仍然缺少风格和安全检查 | 中大型项目但不想用重框架 |
| golangci-lint | 综合 | 覆盖面广、可并行、配置灵活 | 配置复杂度高 | 个人项目、团队项目、CI 门禁 |
我现在做任何 Go 项目,哪怕是三天的 demo,也会把 golangci-lint 加上。开头多花五分钟,后面省下来的排查时间远不止五分钟。
3. 环境准备与第一份配置文件
3.1 安装 golangci-lint 的几种方式
golangci-lint 的安装方式官方文档列了几种,我实测下来最推荐的是用 go install 直接装成二进制,版本跟着 Go 的工具链走,方便统一管理。
bash复制go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
注意一点,go install 需要 Go 版本在 1.18 以上,否则编译不过。装完后确认一下:
bash复制golangci-lint --version
如果显示版本号,说明安装成功。还有一种方式是用官方提供的安装脚本,但因为不同操作系统环境差异比较大,我一般只在没有 Go 环境的 CI 容器里用脚本方式。本地开发环境,我都是直接 go install,因为后续升级只需要改一个版本号重跑一次。
3.2 最小可用的 .golangci.yml
golangci-lint 的配置写在一个叫 .golangci.yml 的文件里,放在项目根目录。这个文件是整套 lint 体系的核心,它决定了哪些规则会生效、哪些会被忽略。我建议新项目从一套比较保守的默认配置开始,跑通之后再逐步加严格规则,不要一上来就在配置里堆上百个 linter,否则误报会多到让你想放弃。
下面是我常用的一份基础配置,标注了每个关键字段的含义:
yaml复制run:
timeout: 2m
modules-download-mode: readonly
tests: true
linters:
enable:
- errcheck
- govet
- staticcheck
- ineffassign
- unused
- bodyclose
- gosec
- revive
disable:
- exhaustruct
linters-settings:
errcheck:
check-type-assertions: true
check-blank: false
govet:
enable:
- fieldalignment
staticcheck:
checks:
- all
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
run.timeout 我设置在 2 分钟,大型项目全量检查可能比较慢,但这个时间足够。modules-download-mode: readonly 表示不会自动去下载新的依赖,避免 lint 过程中悄悄改了 go.mod。
linters.enable 里我默认开启了最常用的几个。errcheck 检查错误是否被处理,govet 是 go vet 的逻辑,staticcheck 做深度静态分析,ineffassign 查无用的赋值,unused 查未使用的代码,bodyclose 专门查 HTTP response body 是否关闭,gosec 做安全扫描,revive 管代码风格。这几个组合起来已经能覆盖绝大多数问题。
exhaustruct 我显式禁掉了,因为它要求结构体所有字段都必须初始化,这在很多业务场景下过于严格,会让代码里充满无意义的零值赋值。
3.3 关键配置字段的取舍原因
配置里有两个字段我想重点展开讲一下,因为它们直接决定你会不会被误报烦到。
第一个是 issues.exclude-use-default: false。这个字段的含义是:是否使用 golangci-lint 默认的排除规则。默认情况下,它会把一些“常见但不太重要”的问题直接过滤掉,比如某些 linter 的 style 建议、文档注释缺失等。如果你设成 false,意味着所有规则的结果都会显示出来,你会看到大量“鸡毛蒜皮”的意见。我的建议是刚开始可以先保持 true,等基础问题修干净了,再改成 false 强制自己看全量结果。
第二个是 linters-settings.errcheck.check-type-assertions: true。它会把类型断言 v, ok := x.(T) 中 ok 未检查的情况也标记出来。很多人觉得类型断言不检查 ok 似乎问题不大,但实际中这个问题很隐蔽:
go复制str := anyValue.(string)
如果 anyValue 不是 string 类型,这一行会直接 panic。而检查 ok 的写法会安全得多。所以我建议把 check-type-assertions 打开,让代码在断言时强制走安全分支。
我遇到过不少团队在用 golangci-lint 的时候,因为配置没调好,导致每次跑 lint 输出几百条“无关紧要”的提示,开发者很快就麻木了,最后干脆不跑。配置的核心不是越多越好,而是要在“检查强度”和“可维护性”之间找到平衡。
4. 核心 linter 逐个拆解:它们到底在查什么
4.1 错误处理三件套:errcheck、govet、staticcheck
在 Go 项目里,错误处理是重中之重。Go 的哲学是错误必须显式返回、显式处理,但人在写代码时总会有疏漏。errcheck 干的事就是把这些疏漏找出来。
它检查两类东西:第一,函数返回值里包含 error,但调用后完全没接收;第二,接收了 error,但没做任何判断。更细节一点,errcheck 还能配置是否检查 defer 语句里的错误、是否检查赋值给空标识符 _ 的错误。
我建议把它配置成:
yaml复制linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
check-blank: true 会把 _, _ = foo() 这种主动丢弃错误的写法也检查出来。有人会觉得这样太严格,但我的经验是:主动丢错误大多是在掩盖问题,真正合理的注释说明情况少之又少,宁可先让它报出来,再人工确认。
govet 就不用多说了,它更多是在编译语义层面查问题。这里我想强调一个容易被忽略的点:govet 不是只检查你自己的代码,它还会检查你调用的库代码吗?不会。govet 默认只检查当前 package 下的文件,这本来是它的边界,但也意味着如果某个第三方库有问题,govet 是管不到的。
staticcheck 算得上是 go vet 的全面加强版。它有几十种检查类型,覆盖了性能、逻辑、风格等。我特别推荐 SA 系列检查,比如 SA1019 会提示你使用了已废弃的 API,SA4006 会指出一个值被赋值后再也没有被使用,SA5001 会检查到 defer 中对错误返回值的忽略。这些检查在我实际项目中抓到过不少“看起来能编译、实际上逻辑有问题”的代码。
4.2 资源和生命周期检查:bodyclose、ineffassign、unused
Go 语言写后端服务时,资源泄漏是高频问题。最常见的就是 HTTP 请求的 response body 忘记关闭。这个 bug 很隐蔽,因为不是每次都触发,只有当连接池里的连接被耗尽时才爆发,一会儿 502、一会儿连接超时,定位起来特别费劲。
bodyclose 就是专门干这个的。它分析 http.Get、client.Do 这类调用,检查 response body 是否在后续代码中被关闭。我自己的项目里,每年都能靠它在 code review 之前拦住三五处 body 泄漏,每一处都可能是线上事故。
ineffassign 查的是无效赋值。看这个例子:
go复制x := 1
x = 2
// 后面再也没用过 x
第一次赋值 x := 1 就是无效的,因为马上被覆盖了。这种代码不影响运行,但它是代码异味,往往意味着开发者改完逻辑后残留了旧行,容易误导后来读代码的人。
unused 查未使用的代码,包括未使用的函数、结构体、变量、常量。这个查出来的东西通常可以直接删。删完代码后跑一下,能帮你清理掉大量“看着有用其实没用”的僵尸代码,降低后续维护负担。
4.3 风格与安全:revive、gosec
revive 是一个风格检查器,它的用户群比 golint 更活跃,规则也更丰富。有人觉得风格检查无所谓,但我的经验是:在多人协作的项目里,风格不统一带来的阅读成本远高于功能本身。
revive 的配置比较细,比如我可以设置 ban-errorf 禁止用 fmt.Errorf 配合 %v 来包装错误,而是推荐使用 %w;也可以设置 function-length 超过多少行的函数就要拆开。这里有个人偏好,不建议全开,先开最核心的几条,比如禁止 goto、禁止空代码块、要求导出函数有注释。
gosec 做的是安全扫描。它检查的不仅是 SQL 注入、命令注入这种明显问题,还包括一些 Go 特有的安全细节。比如:
go复制if err := userInput > 10 {
fmt.Println("invalid")
}
gosec 可能会提示你使用 math/rand 生成加密相关的随机数是不安全的。再比如 http.ListenAndServe(":8080", nil) 这种没有配置超时的写法,gosec 也会提醒你。
我一般不会把 gosec 的告警全部清零,因为有些安全规则在内部工具链里不算实际风险。但我会把它跑着,每次出的告警都会人工过一遍,确认“这里的风险是可接受的”之后再用 //nolint:gosec 注释掉。这种做法比完全不跑安全扫描要负责任得多。
4.4 检查器之间的分工与协作
可能有人会问:这些检查器会不会重复劳动?我的感受是:各有侧重,偶尔重叠,但总体是互补的。govet 和 staticcheck 之间有一点重叠,都以 bug 检测为主,但 staticcheck 覆盖更广。errcheck 和 staticcheck 的某些 SA 系列也会重叠,但一个专注错误处理,一个专注整体逻辑。
这种重叠我认为是好事。就像体检时多个指标都在提醒同一个问题,你更会重视它。所以在配置 linter 时不要刻意避开重叠,重点是让它们都以各自的视角把项目扫一遍。
5. 本地工作流与 CI 集成实践
5.1 本地把 lint 跑起来:Makefile 里的标准命令
配置好 .golangci.yml 之后,本地执行检查的命令是:
bash复制golangci-lint run ./...
如果项目比较大,想快一点,可以加 --fast 参数。--fast 只运行在有缓存里跑得比较快的 linter,比如 govet、errcheck、staticcheck,比较慢的 gosec、revive 等会跳过。适合本地快速验证。完整检查留到 CI 再跑。
我在每个项目的 Makefile 里统一放这几个 target:
makefile复制.PHONY: lint
lint:
golangci-lint run ./...
.PHONY: lint-fix
lint-fix:
golangci-lint run ./... --fix
lint-fix 特别有用。某些问题,比如 gofmt 风格、导入顺序、无用的空行,golangci-lint 可以直接自动修复。跑完 --fix 后再跑一次,剩下的就是要手动处理的问题。这个命令我几乎每天都会用几次,写代码的过程中随手跑一下,修几个自动修复的问题,心里就踏实了。
5.2 想靠人的记性不现实,用 pre-commit 把门
很多开发者连 make lint 都懒得跑,更别提记住在提交代码前执行一下。所以我建议用 pre-commit 框架,在 git commit 之前自动触发检查。
在项目根目录放一个 .pre-commit-config.yaml:
yaml复制repos:
- repo: https://github.com/golangci/golangci-lint
rev: v1.61.0
hooks:
- id: golangci-lint
args: ["run", "./..."]
安装之后,每次 git commit 时 pre-commit 会先跑一遍 lint,有问题就挡住这次提交。这样你在 IDE 里写完代码、准备提交的瞬间,所有问题都会被立刻暴露出来。
有人觉得这个流程很繁琐。我只想说:lint 自动运行的价值不在于“多一道检查”,而在于把“代码质量的后置评审”变成“代码提交的前置门禁”,开发者当场就能修复,才不会在 review 阶段做无意义的来回复制粘贴。
5.3 CI 里做硬性门禁:GitHub Actions 配置实例
本地可以灵活一点,但 CI 里的 lint 必须是硬性要求:跑不过就禁止合并。我现在的做法是在 GitHub Actions 里单独开一个 job 跑 lint,和编译、测试并行。
下面是一份我实际在用的 workflow 配置:
yaml复制name: lint
on:
pull_request:
push:
branches: [main]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.61.0
args: --timeout=5m
这份配置的精髓有两点。第一,它是在 PR 和 push 主干分支时都会触发。PR 触发保证每个合并请求都经过检查,main 分支触发保证即使代码绕过 PR 直接推上来,也会被拦住。第二,args: --timeout=5m 是让我在 CI 上不要把时间卡得太死,staticcheck 这类检查在某些机器上可能比较慢。
在 CI 里如果 lint 挂了,Actions 的运行日志里会直接列出问题文件和行号,开发者点进去就能看到具体是什么规则、为什么报错,修复成本很低。
5.4 团队协作时的“零容忍”策略怎么落
如果团队刚引入 golangci-lint,大概率第一次全量跑会报出几百条问题。这时候怎么办?我的建议是:分阶段推进,不要追求一天清零。
第一步,先把 CI 加上,但允许 lint 只报问题、不阻断合并,让大家先看到问题、适应流程。第二步,每周修一部分存量问题,按目录维度推进,优先修 errcheck 和 bodyclose 这类高风险项。第三步,存量问题修得差不多之后,把 CI 的 lint job 改成硬性阻断,跑不过就不能合并。第四步,把新代码的质量标准拔高,比如 revive 的规则逐条加严。
这个节奏看起来慢,但在团队里反而执行得更持久。你要是一天之内把几百条问题全部清零,要么是团队花了大量时间在修低价值问题,要么就是配了一堆 //nolint 把规则架空了,这两种情况都不是好事。
6. 常见问题与排查技巧实录
6.1 误报太多怎么办?先分辨是真误报还是“我们觉得没必要”
新手最容易在配置阶段被误报劝退。我要说一个残酷的事实:很多所谓误报,其实是检查器逻辑在提醒你“你的代码有隐患,只是现在还没触发”。
举个例子,gosec 会报告:
go复制func hashPassword(pwd string) string {
h := md5.Sum([]byte(pwd))
return fmt.Sprintf("%x", h)
}
它报的是 G401,提示 md5 不安全,建议使用 sha256。对很多内部系统来说,password 的 md5 确实不够安全。这算误报吗?不算,它是合理的安全建议。
真正的误报长什么样?比如 staticcheck 的 SA4017 报告某个函数调用后,返回值 *T 被丢弃了,提示这可能是个 bug。如果你确定丢弃它是有意的,可以加 //nolint:staticcheck 注释并写明理由。
所以我处理误报的方式是:先看规则的官方文档,理解它背后的动机,再决定是改代码还是 //nolint。而不是一开始就把规则关掉。
6.2 nolint 的正确用法:能少用就少用,用了就要说明
//nolint 注释是 golangci-lint 提供的豁免机制。它很灵活,但也很容易被滥用。我见过最夸张的代码是函数开头写一排 //nolint:goerr113,lll,godox,funlen,cyclop,把这个函数的所有约束全关了。这种代码比没有 lint 还危险,因为它变成了一块“法外之地”。
我的原则有三条。
第一,能改代码就不加 nolint。如果 errcheck 报错,说明你漏了错误处理,那就去处理,而不是跳过检查。
第二,只在确定性的边界情况下使用 nolint。比如:
go复制//nolint:gosec // 这里是内部工具,不涉及密码学场景,md5 足够
h := md5.Sum(data)
这种注释有理由、有解释,读代码的人能理解你为什么豁免。
第三,每条 nolint 都要写原因。我甚至建议团队在 code review 时,任何新增的 nolint 注释都要解释清楚。如果解释不出来,就说明它不应该加。
6.3 两个让我印象深刻的线上问题排查
我想分享两个真实案例。
第一个是 HTTP body 泄漏。当时我们的服务是个消息推送中间件,线上偶尔出现连接数缓慢上涨、内存持续增长,但服务看起来没崩溃。我一开始以为是业务代码的问题,后来用 pprof 抓了 goroutine 栈,发现大量 goroutine 阻塞在 http.ReadResponse 的等待中,再往下一查,原来是一个调用第三方接口的函数,resp 读完后没有 defer resp.Body.Close()。bodyclose 之所以没拦住,是因为这个函数在另一个仓库里,那个仓库还没启用 golangci-lint。从那以后,我要求团队所有仓库统一启用 lint,不留死角。
第二个是 shadowed variable 的问题。有一次线上配置变更后功能异常,代码是这样一个 pattern:
go复制cfg, err := loadConfig()
if err != nil {
return err
}
for _, item := range items {
cfg, err := loadItemConfig(item)
if err != nil {
return err
}
process(cfg)
}
这段代码有个大坑。for 循环体里第一个 cfg, err := 用了短变量声明,由于 err 在循环体内是新变量,cfg 也变成了新变量,它遮蔽了外层那个 cfg。我在循环后面想读取全局配置 cfg,结果拿到的还是外层加载的老配置,根本不知道内部其实已经重新加载过。go vet 其实能检查到 shadowed variable 的问题,但因为当时没开启 shadow 检查,这个 bug 在线上一周才被发现。现在我的配置里默认开启 shadow。
6.4 常见问题速查表
我把日常排障中最常遇到的问题整理成了一张表,可以当作速查手册:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| lint 跑出来全是格式问题 | 没跑 gofmt/goimports | 执行 golangci-lint run --fix |
| 某条规则误报告警 | 版本兼容或配置理解偏差 | 查看 linter 官方文档,判断是否豁免 |
| 跑得很慢 | 项目大、缓存未建立 | 本地用 --fast,CI 保证不超时 |
| 相同问题出现几十条 | 代码库历史遗留 | 按目录分阶段修复,不要强求一次搞定 |
| 新库引入但不报错 | CI 只扫了某个目录 | 检查 run.issues 配置范围 |
| CI 与本地结果不一致 | 版本不一致或缓存问题 | 统一 golangci-lint 版本,清缓存重跑 |
| 想屏蔽某条规则但不会写 | 不熟悉配置语法 | 在 linters.disable 里显式关闭 |
6.5 关于版本管理的一点心得
golangci-lint 的版本升级,每次都会带来新规则或规则调整,所以在 CI 里要固定版本。我一般会用一个固定的 major 版本,比如 v1.61.x,然后每年升级一次小版本,升级时专门跑一遍全量 lint,看看新版本带来的告警和误报,再决定要不要调整配置。
还有一个建议:把 .golangci.yml 和 Makefile 的 lint 相关配置纳入 code review 范围。很多团队把这份配置文件当成一次性工具,写完之后没人看。但实际情况是,配置文件和业务代码一样需要演进,需要维护,也需要 review。
注意:
golangci-lint的默认规则集合会随版本变化,升级前建议看官方 release notes,避免你在不确定的情况下因为新规则导致 CI 突然红一片。
7. 从 lint 到代码质量的三个层级
lint 只是代码质量体系的一部分,我把它放在最底层。再往上,还有 code review 和测试覆盖,它们共同构成代码质量防线。
我自己的项目里,lint 解决的是“机器能判断的问题”,比如错误处理、资源泄漏、无效代码、不安全的写法。code review 解决的是“机器判断不了但人能看懂的问题”,比如架构设计是不是合理、依赖方向是否清晰、命名是否恰当地表达了意图。测试解决的是“代码行为是否符合预期”,它验证的是功能正确性。
这三者不是替代关系,而是互补关系。lint 可以减少 code review 里“手指头能看到的问题”,让 reviewer 把注意力放在更高维度的设计讨论上。测试则提供了最终的兜底。
我见过一些团队,把 lint 当成“质量的全部”,lint 跑过了就以为代码没问题。也见过一些团队完全不做 lint,把所有问题都推到 code review 阶段,结果 review 变成了改错别字和补错误处理。这两种做法都不健康。
如果让我给一个建议,我会这么说:lint 是质量体系的门槛,不是天花板。它可以拦住低级的、可自动化的错误,但它拦不住架构设计的缺陷,也替代不了对业务逻辑的深度思考。把 lint 当作日常开发的一部分,但不要神话它。
8. 踩坑之后最重要的一个心得
最后再分享一个小技巧。我在搭建好几套项目的 lint 体系之后,发现最大的踩坑点往往不是配置复杂,而是“一次性配完就再也不看”。golangci-lint 跑一次通过之后,大家就把它当成一个“背景角色”了。但工具会升级,代码会变动,团队会换人,一个从不维护的 lint 配置,迟早会变成摆设。
我的建议是:每个季度抽出半小时,把 .golangci.yml 打开重新读一遍,对照当前代码库里出现的新的问题模式,看看有没有可以补的规则。同时跑一次全量 lint,把积压的问题清一遍。这个习惯花的时间很少,但能让 lint 体系一直保持“活着”的状态。
还有一个小经验,新功能分支在合并之前,顺手在本地跑一遍 golangci-lint run ./... --fix,把能自动修的问题先修了,然后 commit。这样 CI 报出来的问题会少很多,其他人看你的 PR 也会舒服很多。
我从最开始连 go vet 都懒得跑,到现在把 golangci-lint 当成开发流程里的第一道闸门,走了不少弯路。希望这篇文章能帮你少踩一些坑,把你的 Go 项目质量从“靠自觉”提升到“靠机制”。
