with one click
go-idioms
写 Go 时使用。地道 Go:错误处理、并发、接口的正确姿势。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
写 Go 时使用。地道 Go:错误处理、并发、接口的正确姿势。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | go-idioms |
| description | 写 Go 时使用。地道 Go:错误处理、并发、接口的正确姿势。 |
| category | language |
| tags | ["go","golang","惯用法"] |
err、无法退出的 goroutine 或过度抽象的接口时。defer 管理资源或发现循环变量捕获问题时。if err != nil 不忽略;用 %w 包装保留链路规则: 每个返回 error 的调用结果必须检查;向上传递时用 fmt.Errorf("操作说明: %w", err) 包装,保留原始错误供 errors.Is/errors.As 使用;禁止用 _ 丢弃 error。
为什么: AI 在快速生成代码时极易写出 result, _ := db.Query(...)——把错误扔掉,程序继续用一个零值 result 往下跑,最终在几十行后 nil pointer panic,且调用栈完全看不出根因。Go 的设计哲学就是让错误无处可藏,用 _ 丢弃等于主动绕过这层保护。
怎么做:
if err != nil { return ..., fmt.Errorf("xxx: %w", err) }。errors.Is(err, ErrNotFound) 或 errors.As(err, &target);不要字符串匹配。go 启动的 goroutine 要能退出规则: 每个 go func() 启动的 goroutine 都必须有明确的退出路径(通过 context.Done()、关闭 channel 或 WaitGroup);共享内存的并发访问必须用 sync.Mutex/sync.RWMutex 或原子操作保护;不用裸 goroutine 泄漏。
为什么: AI 常见并发 bug:go func() { for { process() } }() 启动后无法停止,服务关闭时 goroutine 还在运行,造成资源泄漏或数据竞争。另一个高频错误是在循环中直接捕获循环变量:go func() { fmt.Println(v) }() —— 所有 goroutine 最终打印同一个 v(Go 1.22 前的经典陷阱,升级版本不等于旧代码变安全)。
怎么做:
ctx context.Context,监听 ctx.Done()。sync.WaitGroup;传结果 → buffered channel,大小与 goroutine 数匹配。go func(v T) { ... }(v)(或升级到 Go 1.22+)。go test -race 在 CI 中检测数据竞争。规则: 接口只包含调用方实际需要的方法(通常 1–3 个);接口定义放在使用它的包,不放在实现包;不提前为"可能有多种实现"预留接口。
为什么: AI 惯用 Java 思维写 Go:在 service 包里定义一个有 15 个方法的 UserService 接口,然后同一个包里只有一个实现。这在 Go 里是反模式——接口越大,满足它的类型越少,测试 mock 越难写。Go 的 io.Reader 只有一个方法,是接口设计的标杆。
怎么做:
-er 结尾(Reader、Closer、Notifier)。defer 释放资源;注意循环变量捕获与 slice 共享底层数组规则: 打开文件/连接后立即 defer f.Close();注意 defer 在循环中会积压到函数返回才执行,循环内的资源要显式关闭或拆成独立函数;对 slice 做 append 或子切片时,理解共享底层数组的副作用。
为什么: 两类经典 AI 错误:①在循环里 defer rows.Close(),以为每次迭代都会关闭,实际上等函数结束才关,数据库连接池被耗尽;②sub := s[1:3],修改 sub 的元素时顺手改了原始 s 的数据,引发难以复现的 bug。
怎么做:
defer,或在每次迭代末尾显式 Close()。copy 出一份,或用 s[1:3:3](三索引切片)限制容量,阻止 append 时共享底层数组被意外扩写。defer 中有可能返回错误(如 f.Close())→ 用命名返回值捕获,不要默默丢掉关闭错误。gofmt 与 Effective Go;不写 Java 风格的 Go规则: 代码必须通过 gofmt(或 goimports)格式化;包名小写单词无下划线;构造函数用 NewXxx;不用 getter/setter 方法封装公开字段(直接暴露字段);错误字符串小写、不加标点。
为什么: AI 极容易把其他语言的风格带进 Go:GetUserName() getter、user_name 下划线命名、errors.New("Database connection failed.") 大写加句号——每一条都违反 Go 官方惯例,过 golangci-lint 都会报警,在 code review 里会被逐条打回。
怎么做:
gofmt -l . 和 golangci-lint run,有 diff 就失败。fmt.Errorf("用户不存在: %w", err) —— 小写开头,不以句号结尾(因为调用方会继续包装)。// 反例 — 错误被丢弃,goroutine 无退出机制
func StartWorker() {
go func() {
for {
rows, _ := db.Query("SELECT ...") // ❌ 忽略 err
process(rows)
// ❌ 永远不会退出,服务关闭时泄漏
}
}()
}
// 正例 — 显式错误处理 + context 控制生命周期
func StartWorker(ctx context.Context, db *sql.DB) error {
go func() {
for {
select {
case <-ctx.Done():
return // ✅ 能干净退出
default:
}
rows, err := db.QueryContext(ctx, "SELECT ...")
if err != nil {
log.Printf("query: %v", err) // ✅ 错误被处理
return
}
process(rows)
rows.Close() // ✅ 循环内显式关闭,不用 defer
}
}()
return nil
}
// 反例 — 实现包里定义了臃肿接口,测试 mock 需要实现全部 15 个方法
package userservice
type UserService interface { // ❌ 15 个方法,放在实现包
GetUser(id int) (*User, error)
CreateUser(u *User) error
UpdateUser(u *User) error
DeleteUser(id int) error
ListUsers() ([]*User, error)
// ... 还有 10 个方法
}
// 正例 — 使用方定义最小接口,测试只需 mock 用到的方法
package orderhandler // ✅ 使用方的包
// 只需要查询用户,定义单方法接口
type UserGetter interface {
GetUser(id int) (*User, error) // ✅ 够用即可
}
func NewHandler(users UserGetter) *Handler { ... }
error 的调用结果都检查了,没有用 _ 丢弃?%w 包装,保留了错误链?go func() 启动的 goroutine 都有通过 context 或 channel 退出的路径?defer 积压到函数末尾?gofmt 格式化,错误字符串是小写且不含句号?做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。