| name | go-pitfalls |
| description | Go 语言陷阱检测:浮点精度、slice/map 内存语义、time/JSON/HTTP 标准库陷阱、 nil 语义、控制流陷阱、容器化性能、string/rune 编码、CPU 缓存友好性。 覆盖 100 Go Mistakes 中 linter 和其他 skills 无法处理的常见错误模式。 Use when writing code involving float comparison, slice/map operations, time/JSON/HTTP usage, string substrings, switch/select/break control flow, nil interface values, Docker/K8S deployment, rune/string iteration, defer with named returns, CPU cache layout, or profiling/diagnostics.
|
Go 语言陷阱
覆盖 linter 和其他 Go skills 检测不到的语言层面陷阱。每个陷阱都有固定的代码模式,AI agent 可以在写代码时主动预防。
检查时机
在以下场景触发检查:
- 浮点数比较 — 看到
== 或 != 作用于 float32/float64
- slice 切分后 append — 看到
s[:n] 后跟 append
- 大 slice 切小子串 — 看到
s[:n] 且 n << cap(s)
- 大字符串切子串 — 看到
s[:n] 且 s 来自外部输入或大文件
- map 频繁增删 — 看到
delete(m, k) 在循环中
- switch/select 内 break — 看到
break 在 switch 或 select 块内
- 返回具体指针类型作为 error — 看到
func() *MyError 返回给 error 接口
- Docker/K8S 部署 — 看到
Dockerfile 或 GOMAXPROCS 相关配置
- time.After 在循环中 — 看到
time.After 在 for 或 select 块内
- JSON 大整数/零值 — 看到
int64 字段反序列化 JSON,或 omitempty 用于零值有意义的字段
- HTTP 默认 client — 看到
http.Get/http.Post 直接调用,或 HTTP handler 中 Write 后无 return
- 字符串按 byte 操作 — 看到
len(s) 用于字符串、s[i] 索引、s[:n] 切片、for i < len(s) 遍历
- defer + 命名返回值 — 看到
defer 函数修改命名返回值
- sync.Cond — 看到 goroutine 忙等轮询共享变量,或需要 Broadcast/Signal 通知的场景
- fmt.Sprintf 触发 Stringer — 看到
%v/%s 作用于可能有锁的类型
- struct 内存布局 — 看到高频分配的 struct 有很多字段,或多 goroutine 写同一 struct 不同字段
- 测试用 sleep — 看到
time.Sleep 在测试中等待异步结果
- getter/setter 惯性 — 看到 Go 代码中为每个字段写
GetXxx()/SetXxx() 方法
核心规则
1. 浮点数不用 == 比较
浮点运算有精度损失,0.1 + 0.2 != 0.3。
if a == b { }
if math.Abs(a-b) < 1e-9 { }
详见 references/float.md
2. slice 切分后 append 可能污染原数组
子 slice 与父 slice 共享底层数组,append 在容量足够时直接覆盖原数据。
sub := s[:2]
sub = append(sub, 99)
sub := make([]int, 2)
copy(sub, s[:2])
详见 references/slice-memory.md
3. 大 slice/map 有内存泄漏风险
- slice 切小段后,整个底层数组无法 GC
- map 删除 key 后不缩容
详见 references/slice-memory.md 和 references/map-memory.md
4. 子字符串保留原字符串内存
huge := readHugeString()
sub := huge[:100]
sub := strings.Clone(huge[:100])
5. break 在 switch/select 内不跳出 for
for {
select {
case <-ch:
break
}
}
loop:
for {
select {
case <-ch:
break loop
}
}
6. nil 指针赋给 interface 不是 nil interface
var p *MyError = nil
var err error = p
err != nil
func doSomething() error {
return nil
}
详见 references/nil-semantics.md
7. 容器化环境必须设置 GOMAXPROCS/GOMEMLIMIT
Go 不自动感知 cgroup 限制。Docker/K8S 中必须显式配置。
# ✅
ENV GOMAXPROCS=4
ENV GOMEMLIMIT=1GiB
或用 automaxprocs / automemlimit。
详见 references/container.md
8. time.After 在 for+select 中会泄漏
每次 time.After 调用创建一个 Timer,超时前不会被 GC。循环中反复调用导致内存飙升。
for {
select {
case v := <-ch:
process(v)
case <-time.After(time.Second):
}
}
t := time.NewTimer(time.Second)
defer t.Stop()
for {
t.Reset(time.Second)
select {
case v := <-ch:
process(v)
case <-t.C:
}
}
详见 references/time.md
9. JSON 大整数精度丢失 + omitempty 吞零值
JSON number 是 float64,超过 2^53 的整数精度丢失。omitempty 会吞掉零值字段。
var v struct { ID int64 `json:"id"` }
json.Unmarshal([]byte(`{"id": 17181928374625163728}`), &v)
var v struct { ID string `json:"id"` }
type User struct {
Age int `json:"age,omitempty"`
}
type User struct {
Age *int `json:"age,omitempty"`
}
详见 references/json.md
10. HTTP DefaultClient 无超时 + handler 缺 return
resp, err := http.Get(url)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
func handler(w http.ResponseWriter, r *http.Request) {
data, err := getData()
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
}
w.Write(data)
}
详见 references/http.md
11. 字符串长度和遍历按 byte 不按 rune
len(s) 返回字节数,s[i] 返回 byte,for range s 按 rune 遍历。处理中文/emoji 等多字节字符时必须注意。
s := "中文"
len(s)
s[0]
for i := 0; i < len(s); i++ { ... }
utf8.RuneCountInString(s)
for _, r := range s { ... }
详见 references/string-rune.md
12. defer 可以修改命名返回值
func f() (result int) {
defer func() {
result++
}()
return 0
}
这个行为在 error handling 中尤其危险:
func getError() (err error) {
defer func() {
if err != nil {
log.Error(err)
}
}()
return doSomething()
}
13. 忘记 sync.Cond
goroutine 忙等轮询共享变量是常见的并发错误。sync.Cond 提供高效的等待/通知机制。
for {
mu.Lock()
if ready {
mu.Unlock()
break
}
mu.Unlock()
time.Sleep(10 * time.Millisecond)
}
cond := sync.NewCond(&mu)
cond.L.Lock()
for !ready {
cond.Wait()
}
cond.L.Unlock()
cond.L.Lock()
ready = true
cond.Signal()
cond.L.Unlock()
14. fmt.Sprintf + %v 可能触发死锁
如果类型实现了 fmt.Stringer 且 String() 方法加了锁,在持锁状态下调用 fmt.Sprintf("%v", obj) 会死锁。
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) String() string {
c.mu.Lock()
defer c.mu.Unlock()
return fmt.Sprintf("count=%d", c.count)
}
func (c *SafeCounter) Incr() {
c.mu.Lock()
c.count++
log.Printf("after incr: %v", c)
c.mu.Unlock()
}
func (c *SafeCounter) Incr() {
c.mu.Lock()
c.count++
n := c.count
c.mu.Unlock()
log.Printf("after incr: count=%d", n)
}
15. Go 不需要 getter/setter
Go 不是 Java。直接暴露字段是惯用做法,只在需要额外逻辑时才写方法。
type User struct {
name string
}
func (u *User) GetName() string { return u.name }
func (u *User) SetName(name string) { u.name = name }
type User struct {
Name string
}
type User struct {
name string
}
func (u *User) SetName(name string) error {
if name == "" {
return errors.New("name required")
}
u.name = name
return nil
}
16. struct 字段顺序影响内存布局
type Bad struct {
a bool
b int64
c bool
}
type Good struct {
b int64
a bool
c bool
}
详见 references/cpu-cache.md
17. 测试中不要用 sleep 等待
go doAsync()
time.Sleep(100 * time.Millisecond)
assert.Eventually(t, func() bool {
return getResult() == expected
}, 5*time.Second, 10*time.Millisecond)
done := make(chan struct{})
go func() { doAsync(); close(done) }()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("timeout")
}
详见 references/time.md
18. 不用 pprof/trace 诊断性能
benchmark 告诉你"慢了多少",pprof 告诉你"慢在哪",trace 告诉你"为什么慢"。
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof ./...
go tool pprof cpu.prof
go test -bench=. -trace=trace.out ./...
go tool trace trace.out
详见 references/diagnostics.md