惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
Visual Studio Blog
G
Google Developers Blog
V
V2EX
The Register - Security
The Register - Security
博客园 - 三生石上(FineUI控件)
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园_首页
S
SegmentFault 最新的问题
博客园 - Franky
Martin Fowler
Martin Fowler
Stack Overflow Blog
Stack Overflow Blog
A
About on SuperTechFans
人人都是产品经理
人人都是产品经理
aimingoo的专栏
aimingoo的专栏
罗磊的独立博客
C
Check Point Blog
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
The GitHub Blog
The GitHub Blog
Last Week in AI
Last Week in AI
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
F
Fortinet All Blogs
Jina AI
Jina AI
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
B
Blog
L
LangChain Blog
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
博客园 - 聂微东
Hugging Face - Blog
Hugging Face - Blog
M
MIT News - Artificial intelligence
GbyAI
GbyAI

See you soon

使用 Quadlet 将 Podman 中的 Postgres 当作 systemd 服务运行 | See you soon 大他者,那个无时无刻都在盯着你的东西 | See you soon Laws of Software Engineering,软件工程定律 | See you soon 浅记多因素身份认证 | See you soon Linux 内核中的度量单位 | See you soon 重置 GPG 智能密钥 | See you soon 向 NAS 引入 samba | See you soon 无法重复键入的 Fcitx5 | See you soon ZFS 降级事故 | See you soon 记被 XanMod Kernel 和 AppArmor 联合坑的一次踩坑 | See you soon agent 的 skill 与 toolcall | See you soon 记一次服务器被挂恶意挖矿二进制 | See you soon 活着的 Arc | See you soon 令 acme.sh 使用 Cloudflare 的 DNS API 签发与续签证书 | See you soon 于 Tokio 中卸载 CPU Bound 任务 | See you soon 如我所见,梦破碎的时候 | See you soon 74LS 家族手册 | See you soon JDK Projects 备忘录 | See you soon 关于历史 | See you soon 用 curl 下载 OnePlus 的 ROM | See you soon 实用命令切片 | See you soon 再见,Oh My Zsh。 | See you soon 博客的明日 | See you soon 被 AppArmor 击杀的 Dockge | See you soon AI 时代的自我 | See you soon 支持删除的布隆过滤器 | See you soon 基于栈的虚拟机与基于寄存器的虚拟机 | See you soon RVA23 包含了什么 | See you soon Rust 的边界检查是否已经有很大的进步 | See you soon 使用 Rust 编写操作系统:Barebones | See you soon 使用 Rust 编写操作系统:引言 | See you soon 编译器笔记:rope | See you soon 编译器笔记:CST | See you soon Paradoxical 札记 | See you soon 简单的相似去重算法(基于向量) | See you soon 更加现代的 PaperMC Minecraft 插件设计指南 | See you soon 简单的 CFG 语法分析方法 | See you soon 简单地使用 Caddy 实现 CORS 配置 | See you soon 让 OpenCV 可以被静态链接 | See you soon 图片搜索笔笺 | See you soon 电路板设计笔记-保护 | See you soon 使用 Rust 实现 SnowflakeId | See you soon 农夫乐事 FaQ 启示 | See you soon 使用 Rust 实现拓展系统札记 | See you soon 遗传学定律的代码实现 | See you soon EN:The Journey of Rust and Procps | See you soon ZH:Rust 与 Procps 之旅 | See you soon 使用 debootstrap 与 schroot 构建一个纯净环境 | See you soon GPG添加新的用户信息 | See you soon GSoC2024 笔记:使用 Rust 重新实现 procps | See you soon JWT 小册 | See you soon 使用密钥登录 SSH | See you soon Guava:Cache | See you soon 编译器笔记:增量编译 | See you soon Gradle秘境:添加一个类似modCompileOnly的依赖块 | See you soon Minecraft:原始经济系统设计简述 | See you soon TinyRemapper笔记 | See you soon MicroOS:进阶 | See you soon MicroOS:起步 | See you soon Swapfile 指北 | See you soon GPG 物理密钥的安装与密钥的迁移 | See you soon 用GPG签名告诉大家这就是我的提交 | See you soon DFS:深度优先搜索 | See you soon 文章翻译:从init.vim到init.lua | See you soon 目录遍历攻击 | See you soon 如何挂载.img文件,以及如何使用QEMU模拟arm64环境 | See you soon 在树莓派上编译 OpenCV | See you soon 关于卸载BlueStacks后遇到的问题 | See you soon
你不应该复用 strings.Builder | See you soon
Krysztal Huang · 2026-01-01 · via See you soon

在编写 Go 程序的时候在程序所占用的堆足够大后经常会遇到 GC 缓慢的问题,在这个时候第一个入手的地方就是利用对象池来处理对象复用问题减轻 GC 压力。

特别是对于 strings.Builder 类型,更应该利用 sync.Pool 来复用它对…吧?

网上是怎么说的?

截止 2025 年 11 月 22 日,在 Bing 上搜索关键字strings.Builder 复用会得到如下几个排在前面的结果

毫不意外地,三篇看起来高质量中文来源的文档都提到了你应该复用 strings.Builder。事实是这样吗?

为什么你不应该复用 strings.Builder?

要继续了解这一点,我们需要知道一个基础知识: string 类型是不可变的,一旦创建不可修改(正常情况下)

如果要进行修改需要使用 unsafe 黑魔法,而大多数情况下应该保持其不可变原则

strings.Builder 的源码

简略起见,这里摘抄本文涉及到的代码即可

// A Builder is used to efficiently build a string using [Builder.Write] methods.

// It minimizes memory copying. The zero value is ready to use.

// Do not copy a non-zero Builder.

type Builder struct {

addr *Builder // of receiver, to detect copies by value

// External users should never get direct access to this buffer, since

// the slice at some point will be converted to a string using unsafe, also

// data between len(buf) and cap(buf) might be uninitialized.

buf []byte

}

// String returns the accumulated string.

func (b *Builder) String() string [](){

return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))

}

// Reset resets the [Builder] to be empty.

func (b *Builder) Reset() {

b.addr = nil

b.buf = nil

}

// copyCheck implements a dynamic check to prevent modification after

// copying a non-zero Builder, which would be unsafe (see #25907, #47276).

//

// We cannot add a noCopy field to Builder, to cause vet's copylocks

// check to report copying, because copylocks cannot reliably

// discriminate the zero and nonzero cases.

func (b *Builder) copyCheck() {

if b.addr == nil {

// This hack works around a failing of Go's escape analysis

// that was causing b to escape and be heap allocated.

// See issue 23382.

// TODO: once issue 7921 is fixed, this should be reverted to

// just "b.addr = b".

b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))

} else if b.addr != b {

panic("strings: illegal use of non-zero Builder copied by value")

}

}

特殊的地方?

我摘抄了三个函数,他们分别是

  • func (b *Builder) Reset():重置状态函数
  • func (b *Builder) String() string:转换成 string 类型的最终函数
  • func (b *Builder) copyCheck():掌管 Builder 不能被复制的神

从这三个函数中就能看到端倪:copyCheck 的存在实际上不允许 Builder 被复制,当出现复制的时候会直接 panic

copyCheck implements a dynamic check to prevent modification after copying a non-zero Builder, which would be unsafe (see #25907, #47276).

We cannot add a noCopy field to Builder, to cause vet’s copylocks check to report copying, because copylocks cannot reliably discriminate the zero and nonzero cases.

copyCheck 实现了一种运行时检查,用于防止在复制了一个非零的 Builder 之后再对其进行修改,因为那样做是不安全的(见 issue 25907, 47276)。

我们不能给 Builder 增加一个 noCopy 字段,从而让 go vet 的 copylocks 检查器报告复制行为,因为 copylocks 无法可靠地区分“零值”和“非零值”这两种情况。

这几段代码中提到了几个 issues,也顺便记录在这里

看起来这里还涉及到了逃逸分析的事情,我们日后再探 :)

黑魔法?

可以很简单的注意到代码切片中有一些 unsafe 用法:

func (b *Builder) String() string [](){

return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))

}

这个写法其实是后来 Go 官方引入的 unsafe 工具,在此之前写法比较像是:

*(*string)(unsafe.Pointer(&b))

其中 b 的类型是 []byte

unsafe.Stringhttps://github.com/golang/go/issues/53003 引入

这个 unsafe 用法非常典型,可以避免一次因为从切片类型转换到 string 类型导致的开销。

但事实上黑魔法总是有代价的,不然为什么叫作黑魔法?

因黑魔法付出的代价

还记得吗?在 Go 中默认情况下 string 类型是不可变的:创建后就不可以修改,如果要修改那么需要付出一次复制内存的代价。

这句话的另外一个意思是:正常创建的 string 对象是不会被修改的。

我们使用以上提到的 unsafe 黑魔法时本质上是对指针进行强制转换,可以这样做的前提是对象的内存布局相同

这里引出了一个问题:如果因为某种情况导致内存布局不同,那么黑魔法就会反噬我们。

在上文提到的 issues https://github.com/golang/go/issues/53003 中也有相关提及:

The second use case is commonly seen as ([]byte)(unsafe.Pointer(&string)), which is by-default broken because the Cap field can be past the end of a page boundary (example here, in widely used code) — this violates unsafe rule (1).

在 Go 中,切片类型是可变的,而 string 类型是不可变的——那我们就得到了一个结论, strings.Builder 在完成构造 string 的时候最终需要构造出一个不可变的类型的指针 string 指向这块内存。

strings.Builder 这个语境上,我们有两个选择:

  • 不复制内部的 []byte,在使用 String() 方法的时候强制转换指向其的指针类型成为 string 作为返回值。
  • 复制内部的 []byte,并且重置内部的 []byte 指针为 nil,将指向原先 []byte 的指针强制转换为 string 作为返回值。

而 Go 选择了第一个方法。为什么呢?

考虑到开销和常见的用法,确实第一种会更合理——因为拼出来的 string 可能会非常大,如果在最后一步还要付出一次复制开销显然是不值得的。

我们继续深入考虑这个选择:当你把指向一块内存的指针从可变类型转换成为了不可变类型,如果你不做额外措施保护这一块内存的话那么他就会受到修改,进而使得产生数据竞争破坏内存。

所以复用 strings.Builder 起码不能减轻因为内存分配而产生的 GC 压力——因为分配的内存块并不能被成功复用。

我们这个时候来看看 Reset() 这个方法,可以清楚的看见这个方法的功能是置为 nil 而不是清空状态,调用 Reset() 后会完全清空其中的内容,这样也就避免了内存块在转换成为 string 受到破坏的可能。

换句话来讲,Reset() 这个方法其实相当于把 strings.Builder 恢复到刚实例化的时候。

所以使用 sync.Pool 复用 strings.Builder 有提升吗?

基于这个想法,我们可以构造多个情况来检测是否有提升。

名字场景描述实现方式
BenchmarkWithoutReset大写入量,串行每次都新建
BenchmarkWithReset大写入量,串行Pool + Reset 复用
BenchmarkWithoutResetTiny极小写入量,串行每次都新建
BenchmarkWithResetTiny极小写入量,串行Pool + Reset 复用
BenchmarkWithoutResetParallel极小写入量,32 并发 goroutine每次都新建
BenchmarkWithResetParallel极小写入量,32 并发 goroutinePool + Reset 复用

我编写的代码如下

package bench_test

import (

"strings"

"sync"

"testing"

)

// 全局的 Pool,避免每次 benchmark 都重新创建

var builderPool = sync.Pool{

New: func() any {

return new(strings.Builder)

},

}

// 为了公平,我们让两个 benchmark 都做同样多的写入工作

const writeSize = 64 // 每次写 64 字节

// 每次新建 Builder

func BenchmarkWithoutReset(b *testing.B) {

b.ReportAllocs()

for b.Loop() {

var buf strings.Builder

for range 100 {

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

}

_ = buf.String()

}

}

// 复用 Builder 并 Reset

func BenchmarkWithReset(b *testing.B) {

b.ReportAllocs()

for b.Loop() {

buf := builderPool.Get().(*strings.Builder)

buf.Reset() // 清零

for range 100 {

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

buf.WriteString("0123456789ABCDEF")

}

_ = buf.String()

builderPool.Put(buf) // 归还

}

}

// 测试非常小分配的情况下不调用 Reset 的情况

func BenchmarkWithoutResetTiny(b *testing.B) {

const tiny = "x" // 1 字节

b.ReportAllocs()

for b.Loop() {

var buf strings.Builder

for range 100 {

buf.WriteString(tiny)

}

_ = buf.String()

}

}

// 测试非常小分配的情况下调用 Reset 的情况

func BenchmarkWithResetTiny(b *testing.B) {

const tiny = "x"

b.ReportAllocs()

for b.Loop() {

buf := builderPool.Get().(*strings.Builder)

buf.Reset()

for range 100 {

buf.WriteString(tiny)

}

_ = buf.String()

builderPool.Put(buf)

}

}

// 测试非常小分配的情况下不调用 Reset 的情况(并行)

func BenchmarkWithoutResetParallel(b *testing.B) {

const tiny = "x"

b.ReportAllocs()

b.RunParallel(func(pb *testing.PB) {

for pb.Next() {

var buf strings.Builder

for range 100 {

buf.WriteString(tiny)

}

_ = buf.String()

}

})

}

// 测试非常小分配的情况下调用 Reset 的情况(并行)

func BenchmarkWithResetParallel(b *testing.B) {

const tiny = "x"

b.ReportAllocs()

b.RunParallel(func(pb *testing.PB) {

for pb.Next() {

buf := builderPool.Get().(*strings.Builder)

buf.Reset()

for range 100 {

buf.WriteString(tiny)

}

_ = buf.String()

builderPool.Put(buf)

}

})

}

运行这个测试,在我的电脑上得出了如下结果:

bench go version

go version go1.24.9 linux/amd64

bench go test -bench=. -benchmem

goos: linux

goarch: amd64

pkg: bench

cpu: AMD Ryzen 9 7945HX with Radeon Graphics

BenchmarkWithoutReset-32 357000 3283 ns/op 24816 B/op 13 allocs/op

BenchmarkWithReset-32 406946 3279 ns/op 24854 B/op 13 allocs/op

BenchmarkWithoutResetTiny-32 2897300 404.1 ns/op 248 B/op 5 allocs/op

BenchmarkWithResetTiny-32 3016304 400.0 ns/op 248 B/op 5 allocs/op

BenchmarkWithoutResetParallel-32 17941327 75.50 ns/op 248 B/op 5 allocs/op

BenchmarkWithResetParallel-32 13661240 90.87 ns/op 248 B/op 5 allocs/op

PASS

ok bench 7.642s

最终的结论

另外我需要提到的是,在部分情况下 string[]byte 是可以不付出转换开销的,可以看这个 issue