Go crypto/aes 怎样减少临时分配
如果 Go 服务在循环里反复调用 crypto/aes,减少临时分配的关键不是把 AES 算法“改写得更底层”,而是把不随消息变化的对象移出热路径,并把 Seal/Open 的结果写回已有容量。最实用的组合是:aes.NewCipher 和 cipher.NewGCM 只初始化一次;每条消息复用目标切片;nonce 每次重新填充,不能因为复用内存而复用 nonce。
官方资料:https://pkg.go.dev/crypto/aes
下面的示例只讨论分配与生命周期,不给出脱离机器和输入规模的“减少了多少百分比”结论。真正的收益要用项目自己的基准和 testing.AllocsPerRun 观察。
先找出临时分配来自哪里
aes.NewCipher 接受 16、24 或 32 字节密钥,分别对应 AES-128、AES-192 和 AES-256;它返回一个 cipher.Block。把这个调用放在每条消息的循环里,会重复创建同一份轮密钥状态。随后每次调用 cipher.NewGCM 也会重复构造 AEAD 包装对象。
// 不推荐:每条消息都重新创建 block 和 AEAD。
func encryptBad(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
// 密钥长度错误时直接返回,不让后续逻辑使用空对象。
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
// 保留 AEAD 构造错误,避免把配置问题伪装成加密失败。
return nil, err
}
nonce := make([]byte, aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
// 随机数读取失败时不能继续使用未填满的 nonce。
return nil, err
}
return aead.Seal(nil, nonce, plaintext, nil), nil
}
这段代码还有一个容易忽略的分配点:Seal(nil, ...) 没有提供可复用的目标容量,结果通常需要创建新的后端数组。它适合演示 API,不适合作为高频批处理的默认结构。
把 NewCipher 和 NewGCM 移出热路径
可以把二者封装在一个长期存活的对象中。对象本身不应该被每个 goroutine 随意修改;如果多个请求共享它,通常只读地共享 cipher.AEAD,把每次请求的 nonce、输入和输出放在请求自己的状态里。
type AESGCM struct {
aead cipher.AEAD
}
func NewAESGCM(key []byte) (*AESGCM, error) {
// 只在配置加载或对象创建时建立 AES 状态。
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &AESGCM{aead: aead}, nil
}
这样做的收益是减少重复初始化,但它不等于“所有缓冲区都能全局共享”。密文、明文和 nonce 都可能被并发请求同时使用,应当按请求、连接或 goroutine 的生命周期管理,不能把同一个可写 slice 直接交给多个并发调用。
用 Seal 的 dst 复用输出容量
cipher.AEAD.Seal 会把密文追加到 dst 并返回更新后的 slice。官方接口还明确说明:如果不再需要明文,可以用 plaintext[:0] 复用明文的后端数组承接密文;如果想保留明文,就准备一个容量足够的独立输出缓冲区。

func (g *AESGCM) EncryptInto(dst, nonce, plaintext, aad []byte) ([]byte, error) {
if len(nonce) != g.aead.NonceSize() {
// 先检查长度,避免把调用错误带到加密内部。
return nil, fmt.Errorf("nonce 长度为 %d,需要 %d", len(nonce), g.aead.NonceSize())
}
// 调用方保证 dst 的剩余容量足够,Seal 只追加密文和认证标签。
return g.aead.Seal(dst, nonce, plaintext, aad), nil
}
func encryptReuse(g *AESGCM, nonce, plaintext []byte) ([]byte, error) {
// plaintext 后续不再使用时,复用它的后端数组作为输出。
return g.EncryptInto(plaintext[:0], nonce, plaintext, nil)
}
这里的 plaintext[:0] 不是复制,也不是清空底层字节;它只是把长度改成 0,保留原有容量。前提是调用者从这一刻起不再读取或修改原明文。若业务还需要记录明文摘要或重试,就应改用独立的 make([]byte, 0, len(plaintext)+g.aead.Overhead()),而不是强行复用。
nonce 可以复用存储,不能复用值
减少分配时可以把固定长度的 nonce slice 放在请求状态里,每次从 crypto/rand.Reader 重新填满它。复用的是字节数组的存储,不是 nonce 的内容。对于同一个密钥,GCM 的 nonce 必须保持唯一;把上一次的 nonce 原样传给下一次,属于安全性错误,不是性能优化。
type EncryptState struct {
nonce []byte
out []byte
}
func NewEncryptState(aead cipher.AEAD, maxPlaintext int) *EncryptState {
// nonce 长度固定;out 预留认证标签所需的额外容量。
return &EncryptState{
nonce: make([]byte, aead.NonceSize()),
out: make([]byte, 0, maxPlaintext+aead.Overhead()),
}
}
func (s *EncryptState) Seal(aead cipher.AEAD, plaintext, aad []byte) ([]byte, error) {
// 每次调用都覆盖 nonce,不能把上一次的 nonce 直接重复使用。
if _, err := io.ReadFull(rand.Reader, s.nonce); err != nil {
return nil, err
}
// 只复用 out 的容量;返回值可能是扩容后的新 slice,要接住它。
s.out = s.out[:0]
s.out = aead.Seal(s.out, s.nonce, plaintext, aad)
return s.out, nil
}
如果状态对象会被多个请求并发使用,就不能共享这里的 nonce 和 out。可以用对象池、每请求状态或每个 worker 的本地状态来隔离写入;对象池归还前还要明确调用方不再持有返回 slice。
解密时用 ciphertext[:0] 回收同一块存储
Open 与 Seal 的目标参数同样是追加目标。密文已经被读取完、调用方不再需要保留它时,可以把 ciphertext[:0] 作为目标,原地承接解出的明文。认证失败时必须检查错误;即便失败,目标容量范围内的内容也可能被覆盖,不能继续把旧明文当作可靠结果。

func decryptReuse(aead cipher.AEAD, ciphertext, nonce, aad []byte) ([]byte, error) {
// ciphertext 后续不再使用,才可以把它的存储作为 Open 的 dst。
plaintext, err := aead.Open(ciphertext[:0], nonce, ciphertext, aad)
if err != nil {
// 认证失败时丢弃返回 slice,不能读取其中可能被覆盖的内容。
return nil, err
}
return plaintext, nil
}
复用边界要记住三条:dst 不能与 additionalData 重叠;目标剩余容量不能以不允许的方式与输入重叠;nonce 和附加数据必须与加密时的语义匹配。省下一次分配,不值得换来难以定位的数据别名问题。
用 AllocsPerRun 观察,而不是凭感觉改代码
建议先保留一个清晰的基线,再比较“重复初始化 + nil 输出”和“复用 AEAD + 预留输出”的版本。基准只用于观察当前机器、Go 版本和输入大小下的分配趋势,不应把一次运行的数字写成通用承诺。
func BenchmarkEncryptReuse(b *testing.B) {
key := bytes.Repeat([]byte{0x42}, 32)
plaintext := bytes.Repeat([]byte("payload-"), 32)
aead, _ := newAEAD(key)
nonce := make([]byte, aead.NonceSize())
out := make([]byte, 0, len(plaintext)+aead.Overhead())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i
上面的基准为了突出切片行为,使用了固定 nonce,不能直接拿作生产加密代码。生产路径必须在每次消息加密时获得唯一 nonce;可以把“随机填充 nonce”和“Seal 的分配”分成两个基准,避免把安全必需的成本误判成可删除的临时分配。
什么时候不要强行复用
- 明文需要在加密后继续使用、异步发送或写入审计队列时,不要用
plaintext[:0]覆盖它。 - 密文需要重试、缓存或同时发往多个下游时,不要用
ciphertext[:0]作为解密目标。 - 多个 goroutine 共享同一个可写状态时,不要为了少一次
make而共享输出和 nonce。 - 输入长度变化很大时,长期保留一个极大的容量可能增加内存占用;可以按请求大小或 worker 生命周期重新规划缓冲区。
可以把最终取舍概括为:复用对象,复用容量,重新生成 nonce;先确保数据生命周期和并发边界正确,再用基准确认分配确实是瓶颈。
相关问题
aes.NewCipher 是否应该每次加密都调用?
不应该。密钥不变时,把它和 cipher.NewGCM 放在初始化阶段;每次调用只传入新的 nonce、明文、附加数据和目标缓冲区。
为什么 Seal 的目标 slice 需要接住返回值?
当剩余容量不足时,Seal 可能扩容并返回新的后端数组。即使你预先复用了容量,也应该写成 dst = aead.Seal(dst, ...)。
nonce 复用会不会只是影响性能?
不会。对同一个密钥重复使用 GCM nonce 是安全性错误。可以复用 nonce slice 的存储,但每条消息都必须填入新的唯一值。
Go crypto/aes 怎样处理可选字段
- 上一篇
- Go crypto/aes 怎样处理可选字段
- 下一篇
- Go crypto/sha3 New256 怎样复用哈希状态
-
- Golang · Go教程 | 3小时前 |
- Go maps.Clone 怎样保留错误信息
- 492浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go maps.Clone 怎样处理可选字段
- 113浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go log/slog.Logger 怎样处理可选字段
- 170浏览 收藏
-
- Golang · Go教程 | 3小时前 | 标准库 · go · Go 错误日志 结构化日志 log/slog slog.Logger
- Go log/slog.Logger 怎样保留错误信息
- 428浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go io.LimitReader 怎样保留错误信息
- 160浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go io.LimitReader 怎样减少临时分配
- 302浏览 收藏
-
- Golang · Go教程 | 3小时前 | 标准库 · 错误处理 · Go教程 · Go io.Reader 二进制协议 可选字段 io.LimitReader io.ReadFull
- Go io.LimitReader 怎样处理可选字段
- 171浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go io.Pipe 怎样判断一次写入对应几次读取
- 315浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go sync.OnceValue 怎样区分零值结果与初始化状态
- 278浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · Go archive/tar TypeDir tar.Writer.WriteHeader
- Go archive/tar.Writer.WriteHeader 怎样写入目录条目
- 243浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go fmt.Appendf 怎样减少临时分配
- 329浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 901次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 907次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 988次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 979次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 802次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

