Skip to content
Go
Go 标准库最佳实践清单

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、Server ReadHeaderTimeout、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 相关等)
  • stdversion analyzer 已在 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 泄漏排查直接用 goroutineleak profile(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,假网络/假时钟集成测试)可用来替换手搭的测试桩