Go 标准库最佳实践清单
编码规范与上线自检速查,按主题分组;每条在后文对应深潜篇有展开与实测依据。版本基线 go1.27.0。
错误处理
- 函数返回
error而不是(bool, error) - 用
fmt.Errorf("...: %w", err)包装底层错误,保留errors.Is/As链 - 用
errors.Is判断哨兵错误,errors.As提取类型错误,不用==比较被包装的错误 - 哨兵错误以
Err开头命名(ErrNotFound) - 错误消息小写开头,不以标点结尾
- 不吞没错误(
_ = doSomething()) - “没有结果"与"查询失败"分开处理(如
sql.ErrNoRows只表示前者) - 需要聚合多个错误用
errors.Join(1.20+),不要手工拼接字符串
并发
- “不要通过共享内存来通信,而应该通过通信来共享内存”
- 首选 channel 做 goroutine 间通信,Mutex 做共享状态保护
-
defer mu.Unlock()紧跟mu.Lock();TryLock只在成功分支解锁 - Context 作为函数第一个参数;
WithCancel/WithTimeout/WithDeadline后defer cancel() - 发送方关闭 channel;多发送方场景改用 context 取消
- 用
go test -race检测竞态条件(并接受它只覆盖被执行路径的局限) - 确保每个 goroutine 都能退出;泄漏用
pprof.Lookup("goroutineleak")(1.27 免实验)定位 - Go 1.23+ 不必再为"忘 Stop 的 Timer 泄漏"焦虑,但长活循环里反复
time.After仍是坏味道:改用AfterFunc或 context 超时 - 一次性初始化用
sync.Once或sync.OnceValue(1.21+),不要手拼atomic.Bool双检 -
sync.Pool只放"丢了也无所谓"的临时对象,不放需要 Close 的资源
测试
- 测试辅助函数第一行
t.Helper() - 用
t.Run+ 表驱动测试 - 用
t.TempDir()代替手动临时文件;t.Cleanup()注册清理 - 用
t.Setenv()代替os.Setenv(注意与t.Parallel、t.Chdir互斥) - 基准测试用
b.Loop(1.24+)替代手写b.N循环 - 需要给被测函数传上下文的,用
t.Context()(1.24+),测试结束自动 cancel - 超时/重试/并发时序逻辑用
synctest(1.25+)写成确定性可复现的测试 - 解析类/输入敏感函数加一个 fuzz target,CI 跑
-fuzz冒烟 - 为公开函数写示例测试(
ExampleXxx) - 核心路径覆盖率 > 80%
I/O
- 优先使用
io.ReadAll、io.Copy、io.ReadFull而非手写循环 - 接收
io.Reader/io.Writer而非具体类型 - 用完
io.Closer后defer Close();写入类资源最后显式检查 Close 错误 -
bufio.Writer使用后Flush(),bufio.Scanner循环后查Err()(并注意 64KB token 上限) - 不受控的源套
io.LimitReader防 OOM - 用
fs.FS接口提高代码可测试性;硬边界用os.Root.FS(),os.DirFS不是 chroot
路径与文件访问
- 用
filepath.Join拼接路径,不手写分隔符 - 用
filepath.WalkDir遍历目录树(不跟随符号链接,err != nil时先判d是否为 nil) - 用户提供的相对路径:
filepath.IsLocal(1.20+)做校验,os.Root(1.24+)做落地——不要用strings.HasPrefix判断"在目录内” - 归档解压逐条目校验名称(防 zip-slip/tar-slip),tar 写入器可开
GODEBUG=tarinsecurepath=0 - 记住
filepath.Match的语义:**静默折叠、Windows 上*能匹配/
压缩与归档
-
gzip.Writer必须 Close(footer),外层再补 Flush 语义检查 - zip 写端给
io.Writer就够(bytes.Buffer/os.File均可),读端才需要io.ReaderAt - tar
FileHeader的Size/Name/Mode自己填对,Close返回 nil 不代表内容完整 - 需要"原始字节透传"用
CreateRaw时,填齐CRC32/UncompressedSize64并在读端注册自定义压缩方法 - 只用标准库能力:bzip2 只能解压,zstd/brotli 要引第三方(golang.org/x 或 GitHub)
database/sql
- 一个数据库全局一个
*sql.DB,启动时设SetMaxOpenConns/SetMaxIdleConns(默认 2)/SetConnMaxLifetime/SetConnMaxIdleTime - 所有方法走
*Context变体,ctx 来自请求链路 - 事务用
BeginTx,事务内只用tx的方法;Commit/Rollback用defer+ 显式错误检查 -
Rows必Close(defer)且循环后查Rows.Err() - 可空列用
sql.Null[T](1.22+)或指针接收,别扫进值类型 -
driver.RawBytes当场拷贝,不跨行持有
外部命令与优雅关停
- 直接执行命令 + 参数数组,不经过
sh -c拼接用户输入 - 需要输出用
cmd.Output();只有流式处理才StdoutPipe,且先读完再Wait -
CommandContext+ 设置Cancel/WaitDelay,保证子进程不拖死关停 - 服务入口用
signal.NotifyContext,收到信号后server.Shutdown(ctx)+ 任务队列Wait
HTTP 客户端与服务端
- 复用
http.Client(并发安全),Transport 显式设置MaxIdleConnsPerHost(默认 2) - 超时四件套:
Client.Timeout、ServerReadHeaderTimeout、handler ctx、拨号/TLS 超时可选项 - 请求体防护:
http.MaxBytesReader+ParseMultipartForm(maxMemory) - Go 1.22+ 用标准
ServeMux的方法与{wildcard}pattern,简单服务不必上第三方路由 - pprof / 内部指标端点只绑内网或加认证
- 出站 URL 先
Parse再白名单校验 host(防 userinfo 伪装与 SSRF),判内网用netip.Prefix而不是IsGlobalUnicast
crypto
- token/验证码/密钥一律
crypto/rand;math/rand/v2只用于打散与压测负载 - AES-GCM 的 nonce 每次 12 字节随机(或 1.24+
NewGCMWithRandomNonce),绝不重用 - 比较 MAC/token 用
subtle.ConstantTimeCompare - 消息认证用 HMAC 标准库流程,口令存储用 pbkdf2(1.24+)/argon2id
- TLS 最低版本
VersionTLS12;InsecureSkipVerify仅开发环境;删掉无效的PreferServerCipherSuites配置 - RSA 加密用 OAEP(PKCS1v15 加密自 1.26 弃用);后量子密钥交换(X25519MLKEM768)1.27 默认已启用,无需手动配置
模板
- 模板随二进制发布:
go:embed+template.ParseFS - 生产 HTML 渲染用
html/template,不要手拼字符串再"自己转义" - 执行期动态改模板用
tpl.Clone();并发只读定型后的 template -
Option("missingkey=error"),别让缺 key 静默出空串 - 渲染进
bytes.Buffer成功后再整体写出(Execute 出错不回滚半截输出)
JSON
- 大整数 ID 用
json:"field,string"或json.Number - 动态类型用
json.RawMessage延迟解析 - 流式处理大文件用
json.Decoder/Encoder - 1.27 默认引擎已是 json/v2:嵌入字段 tag 用
embed(不是 v1 匿名内联写法),校验更严(非法 UTF-8、tag 重复报错) - API 模型初始化 slice 字段,避免 nil→
null与空→[]两种输出
性能
- 先用 pprof 定位再优化;本机基准显示热路径
fmt.Sprintf比strings.Builder慢约两个数量级 -
strings.Builder.Grow()预分配容量;Builder 不可拷贝(实测 panic) - 正则全局预编译(
regexp.MustCompile),RE2 没有回溯,复杂"一条正则"不如代码分步 - 频繁创建的小对象用
sync.Pool,接受 GC 清空 - 结构体字段按对齐从大到小排列(实测 bool+int64+bool 24 字节,重排后 16)
- 反射只用于框架/库代码,业务代码避免
运行时与容器
- 容器内存:
GOMEMLIMIT设在容器 limit 之下留出若干余量(它是软限,语义不是 JVM-Xmx),再配一个合适的GOGC——注意GOGC=off时GOMEMLIMIT仍会生效兜底 - Go 1.25 以下手动设
GOMAXPROCS对齐 cgroup CPU 限额(1.25 起默认感知) - 常驻服务用
runtime/trace.FlightRecorder(1.25+)抓罕见毛刺 - 资源释放靠
defer/显式 Close,finalizer 只做"忘清理"的断言
安全
- pprof 端点不暴露到公网
- 不拼接用户输入到命令与 SQL(SQL 用占位符参数)
- 敏感文件权限
0600(记住 Windows 只有只读位生效,实测) - 上传解压校验条目路径(zip-slip),静态文件服务用
IsLocal/os.Root - 依赖 Go 1.27 的
x509系统证书路径变化做部署验证(Windows/macOS 支持SSL_CERT_FILE)
其他
- Go 1.19+ 用
atomic.Int64等类型化原子,代替atomic.AddInt64 - Go 1.21+ 用
slog代替log(InfoContext不会自动从 ctx 取属性,要自己带) - Go 1.16+ 用
os.ReadFile/os.WriteFile -
go.mod语言版本 ≥ 1.22 后 for 循环变量不再需要tt := tt - 静态文件用
embed嵌入(记住点文件/下划线文件的排除规则与all:前缀) - 所有外部调用都包
context.WithTimeout
Go 1.27 迁移检查
升级或新起项目时逐条核对:
-
go.mod的go指令提到 1.27(语言版本行为按 go 指令切换:循环变量、comparable相关等) -
stdversionanalyzer 已在go vet与go test默认集里(1.27 新增,实测go test也能拦住"用了高于 go 指令的 API"):把这类告警清零再升级 go 指令 - encoding/json 默认引擎已切到 v2:跑全量 JSON 相关测试;嵌入 tag
inline改embed;更严格校验(非法 UTF-8、struct tag 重复字段报错)可能暴露存量坏数据;过渡期可GOEXPERIMENT=nojsonv2回退,验证完尽快摘掉 - asynctimerchan 已移除:
time.Timer/Ticker通道为同步语义,“Reset 前排空通道"的旧写法删掉;依赖旧异步行为的代码(自造 tick 合并逻辑)重点回归 - 泛型方法正式可用(具体类型方法可再带类型参数);接口方法仍不支持,别照博客示例写
-
crypto/tls.Config.Rand、rsa.EncryptPKCS1v15已弃用:测试随机注入改testing/cryptotest.SetGlobalRandom,加密改 OAEP - 第三方 UUID 库可迁标准库
uuid(import 路径是顶层uuid):高写入表主键选 V7 - goroutine 泄漏排查直接用
goroutineleakprofile(1.26 实验、1.27 免实验,实测可用) - GC:1.26 起 Green Tea 为默认,1.27 小对象分配提速——在 1.25 时代调过 GC 参数的服务重新回归压测,别背着一堆历史
-Xmx式参数升级 -
math/rand/v2:全局Seed已删,新代码直接用 v2;v1 存量仅保证可编译 - 基准与压测脚本确认
b.Loop、synctest、httptest.NewTestServer(1.27,假网络/假时钟集成测试)可用来替换手搭的测试桩