当前位置:首页 > 文章列表 > Golang > Go教程 > Go crypto/aes 怎样减少临时分配

Go crypto/aes 怎样减少临时分配

来源:17golang原创 2026-11-05 23:03:05 0浏览 收藏

如果 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] 复用明文的后端数组承接密文;如果想保留明文,就准备一个容量足够的独立输出缓冲区。

Go crypto aes 和 cipher AEAD 复用输出缓冲区的关系说明图
图1:说明图展示 NewCipher、NewGCM 与 Seal 的复用关系;这是静态结构图,不是运行截图。
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] 作为目标,原地承接解出的明文。认证失败时必须检查错误;即便失败,目标容量范围内的内容也可能被覆盖,不能继续把旧明文当作可靠结果。

Go AEAD Seal Open 复用切片和 nonce 安全边界说明图
图2:结构图展示 Seal/Open 的安全复用路径,以及 nonce 重复和切片重叠的禁止边界;这是静态说明图,不是运行截图。
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 的存储,但每条消息都必须填入新的唯一值。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go crypto/aes 怎样处理可选字段Go crypto/aes 怎样处理可选字段
上一篇
Go crypto/aes 怎样处理可选字段
Go crypto/sha3 New256 怎样复用哈希状态
下一篇
Go crypto/sha3 New256 怎样复用哈希状态
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    901次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    907次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    988次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    979次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    802次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码