goimports是gofmt的超集,不仅格式化代码(缩进、括号等),还自动增删import、按标准库/第三方/本地分组、合并声明并排序;而gofmt仅做基础格式化且不处理import。

gofmt 和 goimports 是底线,不是全部。只靠格式化工具压不住烂代码——命名模糊、error裸奔、接口过大、包名套娃,这些地方一错,半年后连自己都不敢动。
怎么让 gofmt + goimports 真正起作用
很多人配了自动格式化却没效果,根本原因是没切到 goimports,还在用原始的 gofmt。
-
gofmt只调缩进、括号、空格;goimports才管 import 增删、分组(标准库 / 第三方 / 本地)、排序 - VS Code 必须显式设置:
"go.formatTool": "goimports",同时打开"editor.formatOnSave"和"editor.codeActionsOnSave": {"source.organizeImports": true} - 安装命令必须带
@latest:go install golang.org/x/tools/cmd/goimports@latest,否则 Go 1.21+ 会报unknown revision latest - 如果某次保存没重排 import,先检查文件顶部有没有
//go:build或//go:generate注释——goimports遇到这类标记会直接跳过整块
包名和目录名为什么必须一致且单数小写
包名不是“分类标签”,是导入路径的语义终点。写错会导致 import 路径冗长、别名泛滥、重构时满屏改名。
- 目录
internal/cacheutil里包名不能叫cacheutil,得叫cache;否则外部要写import "myapp/internal/cacheutil",而实际它只是 cache 的辅助逻辑 - 禁止复数:
handlers→ 改成handler;models→ 改成model(除非你真导出多个 model 类型且它们天然是一组) - 禁止下划线和驼峰:
user_db、userDB都不行,就用user - 如果一个包只导出 1–2 个类型,又和邻近包强耦合(比如
cache和cacheutil),大概率该合并——拆太细反而增加维护成本
error 处理怎么才算不“裸奔”
不是所有 if err != nil { return err } 都对。关键看调用方是否需要区分错误类型、是否要重试、是否要转 HTTP 状态码。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 库函数里禁止
log.Fatal或panic;HTTP handler 里禁止忽略err后返回空结构体 - 需要保留上下文就用
fmt.Errorf("xxx: %w", err);只是透传就直接return err,别套一层fmt.Errorf("%v", err)—— 丢掉%w就断了错误链 - 定义可判断的错误变量:
var ErrNotFound = errors.New("not found"),然后用errors.Is(err, ErrNotFound)判断,别用strings.Contains(err.Error(), "not found") - 日志打印前先判空:
if err != nil { log.Printf("read failed: %v", err) }没问题,但log.Printf("read failed: %v", err.Error())在err == nil时 panic
接口为什么不能按“领域对象”定义,而要按“调用方需求”
定义大接口(比如 UserService 含 8 个方法)看似“完整”,实则锁死实现、mock 成本高、测试脆弱。
立即学习“go语言免费学习笔记(深入)”;
- 按 handler 需求抽:如果某个 handler 只要
GetByID和UpdateStatus,就定义type UserReaderWriter interface { GetByID(int) (*User, error); UpdateStatus(int, string) error } - 已有 2–3 个相似调用点再抽象,别一上来就为单个函数造
Doer接口 - 包内私有类型尽量不传接口,直接传
*User更轻量、调用链更清晰;io.Reader那种通用抽象是例外,不是范本 - 接口名以
er结尾是约定,但前提是它真代表一种能力(Reader、Writer),不是为了凑名而加

















