Skip to content
Go
Go 常见陷阱汇总

Go 常见陷阱汇总

跨主题速查清单:只给"一句话版"的陷阱与纠正,展开讲解、实测过程和完整示例见各主题对应的深潜篇(版本基线 go1.27.0 / windows-amd64,标注"实测"的结论均可在本机复现;平台差异均已注明)。


并发与同步

深潜:../02-并发与同步/

陷阱 后果与纠正
值传递复制含 sync.Mutex 的结构体 锁失效;go vet 的 copylocks 会报
WaitGroup.Add 写在 goroutine 内部 negative WaitGroup counter panic;Add 必须在 go 之前
读锁内再取写锁 死锁,RWMutex 不可重入
sync.Once.Do 内递归调用同一个 Once 死锁
time.After 在循环中频繁创建 “channel 不回收、内存泄漏"只对 Go 1.23 之前成立;1.23 起不再被引用的 Timer/Ticker 可被 GC 回收,1.27 移除 asynctimerchan 回退后这是唯一行为。残留问题是每轮循环重新起算超时,长活循环用 context.WithTimeout 或 AfterFunc
向已关闭 channel 发送 / 重复 close panic;约定"只有发送方关闭”,广播场景用 ctx 取消代替 close
收 channel 不判 ok 分不清"关闭"与"恰好收到零值"
WithCancel 的 cancel 从不调用 goroutine 与 context 树泄漏;defer cancel()
sync.Pool 以为对象一直存在 GC 时全量清空;池里放需要 Close 的资源是经典误用
Mutex.TryLock 失败分支又走统一 Unlock 路径 双重解锁 panic;TryLock 成功才注册解锁
atomic.Value 存不同类型 换动态类型即 panic;固定存指针
context 自定义包装忘转发 Value/Done 上游取消与取值链路静默断掉

时间处理

深潜:../01-基础核心包/time.md

陷阱 后果与纠正
以为 time.Parse 按本地时区 无时区信息时按 UTC;解析墙钟用 ParseInLocation
AddDate(0,1,0) 月末溢出 1 月 31 日加一月归一化成 3 月某日;加完检查或锚定"当月第一天"
用 == 比较两个 time.Time 结构体相等含单调时钟与 Location 表示,同一时刻两种表示可以不等;用 Equal
拿零值时间直接参与业务 零值是公元 1 年;先 IsZero()
AfterFunc 返回的 Timer 忘保存 无法 Stop,回调照跑

错误处理

深潜:../01-基础核心包/errors-and-log.md

陷阱 后果与纠正
用 == 比较哨兵错误 被 %w 包装后失配;errors.Is
返回类型化 nil 指针当 error err != nil 但行为"空指针";错误分支必须返回字面量 nil
errors.As 传非指针目标 直接报 errors: *target must be interface or implement error
错误消息大写开头、句号结尾 包装后拼接难看;小写开头、不加标点
用 panic 做流程控制 panic 只留给程序 bug;库边界 recover 转 error
多错误聚合用字符串拼接 errors.Join(1.20+)保留 Is/As 链,消息按行合并

JSON 与 encoding/json v2

深潜:../04-编码与数据处理/encoding-json.md

陷阱 后果与纠正
interface{} 收大整数 落到 float64,超过 2^53 丢精度;用 json.Number、json:"id,string" 或 RawMessage 延迟解析
omitempty 期待跳过空 struct struct 无"空"概念,照打;要跳过用指针或 json:"-"
忘记 Encoder 默认 HTML 转义 <、>、& 变成 < 之类的转义序列;输出不是给 HTML 用时 SetEscapeHTML(false)
nil slice 输出 null、空 slice 输出 [] API 行为不一致;构造时初始化为空 slice
Unmarshal 进已有值的 struct 是合并不是清空,线上传不到的字段保留旧值
map key 用任意类型 仅 string / 整数 / 实现 encoding.TextMarshaler 的类型可做 key
v1 时代嵌入字段 tag 写 inline 1.27 默认引擎已是 v2,匿名嵌入 tag 改名 embed;v2 校验更严(非法 UTF-8、struct tag 重复直接报错),过渡期可 GOEXPERIMENT=nojsonv2 回退 v1
Encoder 输出没有缩进就比对文本 Encoder.Encode 自带尾换行、Marshal 没有;缩进用 MarshalIndent/SetIndent

gob / XML / CSV / Base64

深潜:../04-编码与数据处理/encoding-gob.md、encoding-xml-csv-base64.md

陷阱 后果与纠正
以为 gob 按字段位置匹配 实际按字段名:调换顺序、删中间字段都兼容;改名/改类型才会静默丢数据
以为 gob Decode 会清空目标 零值字段根本不上传,解码是合并语义,目标旧值会残留
xml tag ,cdata/,innerxml 当普通字段编码 属"裸内容"字段,不生成元素(实测 CDATA 直接贴在根元素内)
指望 xml 解码按命名空间过滤 只看 Local 名(实测带/不带 urn: 的 tag 都能解中);不对称出现在编码方向
csv 读带 BOM 的文件 标准库不剥 BOM,首列名混进 \ufeff;自己 Trim(TrimSpace 也不剥 BOM,实测)
base64 变体混用 Std/URL/RawURL 不匹配时解码失败;编码解码用同一个 Encoding
base64 流式 Encoder 不 Close 尾部填充与 flush 不落盘,读回缺数据

正则(RE2)

深潜:../04-编码与数据处理/regexp.md

陷阱 后果与纠正
用 lookahead / 后向引用 RE2 线性时间设计如此,编译期即报错;靠循环/代码补逻辑,不要指望运行时降级
以为默认是 POSIX 最长匹配 Go 是 leftmost-first:`a
替换串 $name 紧跟字母 $na 会被当成完整组名吞掉;用 ${name} 定界
MustCompile 放在请求路径 编译开销大且 panic 面在热路径;全局变量一次编译
想用 bytes.Regexp 匹配二进制 go1.27.0 不存在(编译 undefined);只有 string 入口

模板(text/html template)

深潜:../04-编码与数据处理/text-template.md

陷阱 后果与纠正
并发复用未成型的 template 执行(Execute)并发安全,构造(Parse/AddFuncMap)不安全;定型后 Clone 或先构造完再发布
忽略 Execute 的返回错误 出错前已写进 writer 的半截输出不回滚;先渲染进 buffer 再一次性写出
缺 key 时默不作声 text 输出 <no value>、html 输出空串(实测默认值如此);生产用 Option("missingkey=error")
写 template.JSScript / template.URLS 不存在(实测编译报错);safe 类型只有 JS/URL/CSS/HTML/HTMLAttr/Template 一族
自定义函数返回 string 塞原始 HTML 被上下文感知转义(或被替换成 #ZgotmplZ);要放行必须返回对应的 safe 类型

crypto:哈希 / HMAC / AEAD / TLS

深潜:../04-编码与数据处理/hash-mac-aead.md、../03-网络编程/crypto-tls.md

陷阱 后果与纠正
AES-GCM 重用 nonce 等于重用密钥流:实测两段密文异或 == 两段明文异或;nonce 每次 12 字节随机,或 1.24+ NewGCMWithRandomNonce
== 比较 MAC / token 时序侧信道;crypto/subtle.ConstantTimeCompare(长度不等短路返回,只泄漏长度)
HMAC 前自己先把长 key 哈希一遍 RFC 2104 内部本来就这么做(实测逐字节一致);重复处理会与他人实现不一致
hash.Sum(nil) 与 Sum(dst) 混用 Sum 不改变哈希状态、把摘要追加到 dst;复用缓冲区传 Sum(b[:0]) 是合法优化,拿返回值当输入别名则错
用 math/rand 生成 token/验证码 可预测;必须 crypto/rand
rsa.EncryptPKCS1v15 Go 1.26 起弃用;加密用 OAEP
还在配置 TLS.PreferServerCipherSuites 客户端侧无效 legacy 字段,删掉
TLS 测试注入随机 Config.Rand 1.26 起弃用;用 testing/cryptotest.SetGlobalRandom
口令直接 sha256 入库 可被 GPU 爆破;用 pbkdf2(1.24 进标准库)或 argon2id
判 cipher.ErrOpen 该变量不存在(go doc 核实);GCM 认证失败是文本错误 cipher: message authentication failed

rand / uuid

深潜:../01-基础核心包/strconv-math-rand.md、uuid.md

陷阱 后果与纠正
迁移 v2 时还写 rand.Seed v2 顶层没有 Seed(自动随机种子);v1 也早已自动播种
一个 *rand.Rand 塞给多个 goroutine v1 的 Rand 非并发安全;每 goroutine 一份或用顶层函数
热路径用 v1 顶层函数 v1 全局函数内部带锁;负载敏感的抖动场景用 v2 本地实例
按 crypto/rand/uuid 找包 Go 1.27 新包 import 路径是顶层 uuid(go doc 原文 // import "uuid")
UUID V4 直接当数据库主键 随机序导致索引页分裂;时间有序的 V7 更适合(标准库 V4/V7 都有)

I/O

深潜:../05-文件与系统IO/io-interfaces.md、bufio.md、os-file.md

陷阱 后果与纠正
以为 Read 一次返回全部请求字节 短读合法;定长用 io.ReadFull,区分 io.EOF(什么都没读到)与 io.ErrUnexpectedEOF(读到一半断了,实测)
只看 err 丢掉 n>0 的返回值 Read 可以同时返回数据和 EOF;先处理 buf[:n] 再判 err
bufio.Writer 忘 Flush 数据滞留缓冲区;Flush 的错误也要查
bufio.Scanner 不查 Err() 出错时静默少行;另有单 token 64KB 上限(实测报 bufio.Scanner: token too long,Buffer(nil, n) 扩容)
不受控源直接 io.Copy 无上限读取;套 io.LimitReader
os.Create 以为追加 默认截断;追加用 O_APPEND
写关键数据全靠 defer f.Close() Close 的错误被丢弃;写入类资源最后要显式检查 Close
os.Exit 路径上挂了一堆 defer 全部不执行;清理走正常 return 或显式调用
HTTP resp.Body 不关/不读尽 连接无法复用(1.27 起 Client 会自动 drain 一小段,但别依赖——显式 Close 仍必须)

路径与文件系统

深潜:../05-文件与系统IO/path-filepath.md、os-file.md

陷阱 后果与纠正
dir + "/" + name 手工拼路径 分隔符平台差异;filepath.Join(内含 Clean)
用 strings.HasPrefix 判断"在目录内" 兄弟目录前缀也算命中(/data/uploads-secret 匹配 /data/uploads);校验用 filepath.IsLocal(Go 1.20),落地用 os.Root(Go 1.24)
以为 Match 的 ** 报错或跨目录 连续 * 被静默折叠成 *(实测 Match("a**c","abc") 为 true);Glob("t/**/*.go") 恰好三层(实测只命中 t/nested/x.go)
以为 * 永远不匹配 / filepath.Match 只拒绝本平台分隔符:Windows 上 Match("a**c","a/c") 实测为 true;path.Match 才始终把 / 当分隔符
跨包比较 ErrBadPattern filepath 与 path 是两个不同对象(实测 == false);io/fs 根本没有该哨兵(引用即编译 undefined)
把 IsLocal 当访问控制 不拦内嵌 NUL(实测 true)、不判存在性、不跟符号链接;它是输入校验,硬边界交给 os.Root
EvalSymlinks 后再打开 TOCTOU 窗口;防逃逸要用 os.Root 圈定
Windows 保留名 IsLocal("com1") 为 false 但 "com1.txt" 为 true(实测,拦裸保留名);Base("") 返回 "."、Ext(".hidden") 返回 ".hidden"

压缩与归档

深潜:../05-文件与系统IO/compress-archive.md

陷阱 后果与纠正
gzip.Writer 不 Close footer 丢失,实测只剩 10 字节头,读回 io.ErrUnexpectedEOF;Close 不能省
以为 gzip 流里只有一个成员 Multistream 默认开启会拼接多成员(实测读出 "stream0stream1");它是 Reader 方法不是字段
以为 zip 写端需要 io.WriterAt 写端只要 io.Writer(实测 bytes.Buffer 即可写出合法 zip);只有读端要 ReaderAt
CreateHeader 校验条目名 什么都不校验:反斜杠原样入库、/etc/passwd 返回 nil(实测);zip-slip 的防线必须自己做
解压不校验条目名 Join(dest, 条目名) 实测被 ../ 形态逃逸落盘;IsLocal + os.Root 组合
CreateRaw 不填 size/CRC 一路返回 nil、条目读回空串——静默丢数据(实测);三字段填齐并在读端 RegisterDecompressor
以为 zip 时间"没有时区" 扩展时间戳总是写出、读端优先采用(GOROOT struct.go 注释原文),实测 +0800 时区可往返、亚秒截断;零值时间读回 1979-11-30 +0000 UTC
tar 忘填 Size 写入当场报 write too long,且 Close 返回 nil(实测)——头字段自己填对
以为 tar 默认拦 .. 路径 GODEBUG=tarinsecurepath 默认放行(实测);=0 才报 tar.ErrInsecurePath,且符号链接 Linkname 不受校验
想用 bzip2 压缩 compress/bzip2 只有解压;标准库没有 zstd/brotli/xz

database/sql

深潜:../05-文件与系统IO/database-sql.md

陷阱 后果与纠正
把 *sql.DB 当一条连接 它是连接池:全局开一个长期复用;Tx 独占一条 Conn
事务里调 db.Query 走的是另一个连接,池满即自锁;tx 内只用 tx 方法
以为 BeginTx 后 ctx 取消要自己兜底回滚 包经 awaitDone 自动回滚(sql.go 源码),之后再 Commit/Rollback 只会拿 sql.ErrTxDone(实测)
ctx 取消必断连接 驱动实现 driver.Validator+SessionResetter 时走 keepConnOnRollback 保留连接(sql.go:1906-1910,实测连接关闭数不增)
把 ErrNoRows 当系统故障 QueryRow 无结果是 sql.ErrNoRows,与真错误要分诊;列表查询用 Query 循环
Rows 只 Close 不查 Err 迭代中断开只在 Err() 暴露;两个都要
跨行持有 driver.RawBytes 底层缓冲逐行复用,晚读会混进别行内容(实测第二行截出 second-payloa);要留存当场拷贝
Prepare 以为全局一次 按连接惰性重制(实测两连接各 prep 一次),连接重建会再 prep
默认连接池参数上线 SetMaxIdleConns 默认 2(sql.go:965);按 DB 上限显式设置四个参数
NULL 扫进 string 直接报错(实测报错原文);用 sql.Null[T](1.22)或指针
自写驱动实现 Rows 1.27 起实现 RowsColumnScanner 就不会再被调 Next(driver.go:456 原文)

os/exec 与信号

深潜:../05-文件与系统IO/os-exec.md

陷阱 后果与纠正
以为 exec.Command 找不到命令就报错 签名不带 error:LookPath 失败存进 cmd.Err,延迟到 Start/Run/Output 爆发(errors.Is(err, exec.ErrNotFound) 实测 true)
StdoutPipe 后直接 Wait 丢数据/死锁:必须先读完再 Wait;拿不准就用 Output/CombinedOutput(安全默认)
StdinPipe 忘 Close 子进程等 EOF 永远挂着
超时后想 errors.Is(err, context.DeadlineExceeded) Windows 实测返回 "exit status 1" 且 Is 为 false;Unix 是 signal: killed(按文档,本地未复现)——跨平台按 errors.Is + 文本双重兜底
子进程不退出导致 Wait 挂死 CommandContext + WaitDelay 组合(Cancel 返回值与 WaitDelay 四种搭配见深潜篇)
sh -c "用户输入" 注入面;能直接执行就别经过 shell
signal.Notify 无缓冲 channel 收不过来丢信号;NotifyContext(HTTP 优雅关停配方在深潜篇)

embed 与 io/fs

深潜:../05-文件与系统IO/fs-embed.md

陷阱 后果与纠正
以为 embed 目录会带上点文件 裸目录模式排除 ./_ 开头项;assets/* 通配包含点文件、all: 取消排除(实测三分法)。template.ParseFS 对被排除文件报 pattern matches no files
以为未使用的 embed 变量编译失败 照样编译过(实测);真错误只有 go:embed cannot apply to multiple vars 这类
把 os.DirFS 当 chroot 官方文档明言不是:符号链接可逃逸、相对 root 受 Chdir 影响;能力断言实测只有 ReadDirFS/ReadFileFS/StatFS/ReadLinkFS
errors.Is 区分"非法路径"与"不存在" 同一个 .. 输入,embed.FS 给 fs.ErrNotExist、os.DirFS 给 fs.ErrInvalid(实测分歧);别依赖错误形状
拿嵌入文件 ModTime 做缓存协商 embed 文件 ModTime 是零值,http.FileServer 的 Last-Modified 语义失效;用 ETag/内容哈希

网络:net / http / url

深潜:../03-网络编程/

陷阱 后果与纠正
每次请求新建 http.Client 连接池碎片化;Client 并发安全,全局复用
不设超时三件套 Client.Timeout + Server Read/WriteTimeout + handler ctx;只设一头不够
高并发出网不设 Transport DefaultMaxIdleConnsPerHost 只有 2,其余连接建了又拆;显式调 MaxIdleConnsPerHost
请求体不设上限 http.MaxBytesReader + ParseMultipartForm 的 maxMemory;1.27 会自动 drain 小 Body,但防护仍要自己做
以为路由 pattern 全匹配 Go 1.22+ ServeMux 原生支持 "GET /a/{id}" 方法与通配——写方法就只匹配该方法,简单服务无需第三方路由
IPv6 手拼 "::1:8080" 歧义;net.JoinHostPort 自动加方括号
用 IsGlobalUnicast 过滤内网 192.168.1.1 实测返回 true(它就是全球单播语义);SSRF 防护要自己按 netip.Prefix 判 CIDR
query 参数里的 + Values.Encode 把空格编成 +、解码也按 +→空格;path 场景该用 PathEscape,两边不对称(实测)
拿 URL.Host 直接外呼 http://good.com@evil.com 的 userinfo 伪装;先 Parse 再白名单校验 host,或看 URL.Redacted 心智模型

反射

深潜:../06-系统与测试/reflect.md

陷阱 后果与纠正
对不可寻址的 Value 调 SetXxx panic;传 &x 后 Elem() 再设
Int()/String() 不判 Kind 直接调 kind 不符 panic;先 Kind()
reflect.TypeOf(nil) 与 ValueOf(nil) 混为一谈 前者返回 nil Type、后者返回 invalid Value,再调方法 panic(实测两种行为)
FieldByName 没找到以为会 panic 返回零值 Value,IsValid() 才是判据
TypeOf(x).String() 硬编码比较类型名 用 reflect.TypeFor[T]()(1.22+)做类型比对

测试与性能分析

深潜:../06-系统与测试/testing.md、pprof.md、runtime-debug.md

陷阱 后果与纠正
在子 goroutine 里调 t.Fatal 非法(只有测试 goroutine 能终止测试);goroutine 里用 t.Error + channel 回传
并行测试里调 t.Setenv / t.Chdir panic;这类改全局状态的 API 与非并行配对使用
手写 for i := 0; i < b.N; i++ 1.24+ 用 b.Loop:编译器负责防优化与计时精度;但 b.Loop 循环里 break/提前 return 是雷——实测无条件 break 直接 FAIL(benchmark.go:225: benchmark function returned without B.Loop() == false (break or return in loop?)),带条件的 break 甚至可能 PASS 却只量到 break 本身的开销(实测 1.5 ns/op 的无意义数字)
-race 过了就宣称无并发问题 只报告被执行路径触发的竞态;不检测死锁与逻辑竞态
用 runtime.SetFinalizer 兜底释放资源 不保证何时跑、进程退出前不跑;finalizer 适合"断言已清理",不适合做清理机制
pprof 端点开到公网 泄露内部路径与 goroutine 全量栈;只挂内网/加认证
排查 goroutine 泄漏只看 NumGoroutine 1.27 起 pprof.Lookup("goroutineleak") 免实验可用(实测),能区分"存活"与"泄漏"
GOGC 理解成堆上限 它是"相对上一次活堆的增长百分比";容器内存约束用 GOMEMLIMIT(软限,语义不是 JVM -Xmx)
容器里不设 GOMAXPROCS 也以为没事 Go 1.25 前运行时不感知 cgroup CPU 限额;1.25 起默认感知(官方 notes),旧版本要手动设

泛型

深潜:../01-基础核心包/泛型详解.md

陷阱 后果与纠正
以为 comparable 不能当 map key 的约束 可以("comparable 不可作类型参数"是过时误传);接口动态值里装不可比较类型才会运行时 panic
泛型函数体内对 T 为所欲为 只能使用约束允许的操作;要方法就定义接口约束
给接口方法声明类型参数 1.27 泛型方法正式支持的是具体类型的方法;接口方法仍不支持
继续引 constraints.Ordered 标准库自 1.21 起用 cmp.Ordered;x/exp 仅存量代码

语言与其他

深潜:../04-编码与数据处理/bytes-and-strings.md、../08-计算机基础/计算机基础知识点.md

陷阱 后果与纠正
go.mod 语言版本 < 1.22 的循环变量捕获 按语言版本(非编译器版本)决定共享/逐次;升 go 指令才免 tt := tt
nil map 赋值 panic;读安全、写必须 make
append 不接返回值 / 两个 slice 共享底层数组 扩容前后别名行为不同;别名敏感时显式 slices.Clone
copy(dst, src) 以为补齐长度 静默按 min(len) 截断
if/for 内 := 遮蔽外层 err 外层永远 nil;显式 = 赋值
热路径 fmt.Sprintf 本机基准比 strings.Builder 慢约两个数量级(3002 allocs vs 13);用 strconv + Builder
strings.Builder 拷贝已写过的实例 实测 panic: strings: illegal use of non-zero Builder copied by value;别塞进按值传递的结构体字段或 Pool
以为 TrimSpace 剥掉一切空白 unicode.IsSpace 含 NBSP/全角空格(实测会裁),不含 BOM(\ufeff)与零宽空格(实测原样保留)
binary.Encode/Decode/Append 记成 Go 1.22、记成流式 API Go 1.23 引入(api/go1.23.txt #60023);操作 []byte 缓冲区(buf 太小报错),io 流式风格是老函数 binary.Read/Write

实战建议

  • 这张表用来代码评审与事故复盘时逐条对照,不是用来学习原理的——每条都有一句话结论,看懂之后请到对应深潜篇看实测过程与完整示例。
  • 上生产前的固定动作:go vet ./...(copylocks、stdversion、hostport 等 analyzer 能拦掉表中相当一部分)、并发代码必过 go test -race、时间敏感逻辑用 synctest 写成可确定复现的测试。
  • 表中凡是"实测"的结论都基于 go1.27.0 / windows-amd64;涉及 Linux/macOS 差异的行均已单独标注,跨平台部署前对这几行(exec 超时文本、Windows 保留名、sendfile、大小端相关的系统调用路径)再过一遍。