gofmt 和 goimports 的区别:为什么 CI 里的格式化检查会失败
一句话结论:gofmt 只调整代码排版(缩进、对齐、空格),从不触碰 import; goimports = gofmt + 自动增删 import + import 分组排序。本地编辑器用 goimports、 CI 里却用 gofmt -l 检查(或反过来),是「本地好好的、CI 挂了」的最常见原因。统一用 goimports。
一、gofmt 做什么
gofmt 是 Go 官方的代码排版工具,随工具链一起安装,职责只有一个:让代码的空白布局符合统一规范。 它处理缩进(tab)、对齐、运算符两侧的空格、括号位置这类问题,不会改动任何代码语义。
常用旗标:
gofmt -l . # 只列出「不符合规范」的文件,不改文件(CI 检查就用它)
gofmt -d main.go # 显示会改成什么样的 diff,不写回
gofmt -w . # 直接改写文件
gofmt -s -w . # 在排版之外再做代码简化(如 x[a:len(x)] → x[a:])
gofmt -r 'a[i], a[j] = a[j], a[i] -> a[i], a[j] = a[j], a[i]' -w . # 按规则重写(极少用)
需要记住的边界:gofmt 不会增删 import,也不会调整 import 的分组顺序。 一个引入了却没使用的包,gofmt 会原样保留(编译器会报错,但那不是 gofmt 的职责)。
二、goimports 多做的两件事
goimports 不属于 Go 标准工具链,需要单独安装:
go install golang.org/x/tools/cmd/goimports@latest
它先做 gofmt 的全部工作,然后多做两件事:
- 自动补全和删除 import:代码里用了
fmt.Println但没 import "fmt", 它会补上;import 了没用的包,它会删掉。 - import 分组排序:按「标准库一组、第三方一组」分组,组内按路径字母序排列。
看个典型例子。下面这份代码 import 了没用的 os,分组也是乱的:
package main
import (
"github.com/gin-gonic/gin"
"fmt"
"os"
)
func main() {
fmt.Println(gin.Version)
}
gofmt 的输出:只把 "fmt" 按字母序排进同一组,os 原样保留:
import (
"fmt"
"github.com/gin-gonic/gin"
"os"
)
goimports 的输出:删掉未使用的 os,并把标准库和第三方分成两组:
import (
"fmt"
"github.com/gin-gonic/gin"
)
同一份输入,两个工具的输出不一样——这就是「格式化检查为什么有时过有时不过」的根源。
三、结果会不同的三种典型场景
1. 存在未使用的 import
gofmt 保留,goimports 删除。本地编辑器配的是 goimports(保存时自动删), 提交后 CI 用 gofmt -l 检查能通过;但反过来——本地用 gofmt、CI 用 goimports -l——就会挂。
2. 缺少 import
goimports 会尝试自动补全。存在多个候选包时(比如 rand 可能是
math/rand 也可能是 math/rand/v2),它会按索引猜一个,
猜错了就是编译错误。所以 goimports 的补全结果必须过一眼,别盲目提交。
3. 你自己公司的包被归错组
goimports 默认把「非标准库」全归进第三方组,于是 git.company.com/team/utils
会和 github 依赖混在一起。用 -local 参数把自家前缀单独成组:
goimports -local git.company.com -w .
编辑器里也要同步配置(VS Code 的 go.formatTool 与
gopls 的 local 设置),否则编辑器格式化结果和命令行不一致。
四、CI 里的正确姿势
原则只有一条:本地编辑器、命令行、CI 三处用同一个工具、同一组参数。 推荐统一为 goimports(它是 gofmt 的超集,检查更严):
# 有输出即代表有文件不符合规范,退出码置 1
files=$(goimports -local git.company.com -l .)
if [ -n "$files" ]; then
echo "以下文件未通过 goimports 检查:"
echo "$files"
exit 1
fi
GitHub Actions 完整示例:
name: check
on: [push, pull_request]
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod # 与仓库 go.mod 保持一致,避免版本差
- run: go install golang.org/x/tools/cmd/goimports@latest
- run: |
files=$(goimports -l .)
test -z "$files" || { echo "need goimports: $files"; exit 1; }
- run: go vet ./...
- run: go test ./...
版本坑:不同 Go 版本自带的 gofmt 在极少数边缘语法上的排版会有差异,
goimports 不同版本的分组行为也调整过。CI 用 go-version-file: go.mod
锁定与仓库一致的 Go 版本,可以消掉「我本地格式化过了为什么 CI 还挂」的整类问题。
五、想要更严格:gofumpt 与 staticcheck
- gofumpt:gofmt 的更严格超集(如强制多行 import 块的写法)。 注意全仓库只能选一个——混用 gofmt 和 gofumpt 会产生反复横跳的 diff。
- staticcheck:不是格式化工具,是深度静态检查,CI 里常与格式化检查并列挂:
go install mvdan.cc/gofumpt@latest
gofumpt -l .
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
六、小结
| gofmt | goimports | |
|---|---|---|
| 安装 | 随 Go 工具链自带 | go install golang.org/x/tools/cmd/goimports@latest |
| 代码排版 | ✔ | ✔(完全一致) |
| 增删 import | ✘ | ✔ |
| import 分组排序 | ✘ | ✔(-local 指定自家前缀) |
| CI 检查命令 | gofmt -l . | goimports -l .(推荐) |
更多命令的用法与坑点(gofmt 的 -s/-r、goimports 的索引机制)见 Go 工具链命令速查表的「格式化与代码工具」一节。