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 的全部工作,然后多做两件事:

  1. 自动补全和删除 import:代码里用了 fmt.Println 但没 import "fmt", 它会补上;import 了没用的包,它会删掉。
  2. 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.formatToolgoplslocal 设置),否则编辑器格式化结果和命令行不一致。

四、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 ./...

六、小结

gofmtgoimports
安装随 Go 工具链自带go install golang.org/x/tools/cmd/goimports@latest
代码排版✔(完全一致)
增删 import
import 分组排序✔(-local 指定自家前缀)
CI 检查命令gofmt -l .goimports -l .(推荐)

更多命令的用法与坑点(gofmt 的 -s/-r、goimports 的索引机制)见 Go 工具链命令速查表的「格式化与代码工具」一节。

相关阅读

问题反馈

本站是纯前端静态站,没有后端也没有账号系统,反馈走 GitHub Issues、讨论区或邮件。

粘贴到 issue 或邮件里能帮我更快定位问题,其中不含你输入的任何内容

其它:查看已有反馈 · 邮件反馈(无需 GitHub 账号):278975598@qq.com