Golang 源码解读 01、深入解析 strings.Builder、strings.Join
本文从我的《The Go Programming Language》学习笔记中分离出,单独成一篇文章方便查阅参考。
strings.Builder 源码解析
存在意义
使用 \(strings.Builder\),避免频繁创建字符串对象,进而提高性能
\(Source\ file\) https://go.dev/src/strings/builder.go
与许多支持 \(string\) 类型的语言一样,\(golang\) 中的 \(string\) 类型也是只读且不可变的( \(string\) 类型笔记 Go xmas2020 学习笔记 04、Strings - 小能日记 - 博客园 )。因此,通过循环字符串切片拼接字符串的方式会导致大量的string创建、销毁和内存分配。如果你拼接的字符串比较多的话,这显然不是一个正确的姿势。
在 \(strings.Builder\) 出来以前,我们是用 \(bytes.Buffer\) 来进行优化的。
func main() {
ss := []string{
"A",
"B",
"C",
}
var b bytes.Buffer
for _, s := range ss {
fmt.Fprint(&b, s)
}
print(b.String())
}
这里使用 var b bytes.Buffer 存放最终拼接好的字符串,一定程度上避免上面 \(str\) 每进行一次拼接操作就重新申请新的内存空间存放中间字符串的问题。
但这里依然有一个小问题: \(b.String()\) 会有一次 \([\ ]byte -> string\) 类型转换。而这个操作是会进行一次内存分配和内容拷贝的。
实现原理
Golang 官方将 \(strings.Builder\) 作为一个\(feature\) 引入。
- 与 \(byte.Buffer\) 思路类似,既然 \(string\) 在构建过程中会不断的被销毁重建,那么就尽量避免这个问题,底层使用一个 \(buf\ [\ ]byte\) 来存放字符串的内容。
- 对于写操作,就是简单的将 \(byte\) 写入到 \(buf\) 即可。
- 为了解决 \(bytes.Buffer.String()\) 存在的 \([\ ]byte -> string\) 类型转换和内存拷贝问题,这里使用了一个\(unsafe.Pointer\) 的内存指针转换操作,实现了直接将 \(buf\ [\ ]byte\)转换为 \(string\) 类型,同时避免了内存充分配的问题。
- 如果我们自己来实现 \(strings.Builder\) , 大部分情况下我们完成前3步就觉得大功告成了。但是标准库做得要更近一步。我们知道 Golang 的堆栈在大部分情况下是不需要开发者关注的,如果能够在栈上完成的工作逃逸到了堆上,性能就大打折扣了。因此,\(copyCheck\) 加入了一行比较 \(hack\) 的代码来避免 \(buf\) 逃逸到堆上。Go 栈、堆的知识可看 GopherCon SG 2019 "Understanding Allocations" 学习笔记 - 小能日记 - 博客园
常用方法
- String方法返回Builder构建的数据
- Len方法返回字节数组占据的字节数,1个汉字三个字节
- Cap方法返回字节数组分配的内存空间大小
- Reset方法将Builder重置为初始状态
- Write方法将字节数组加添加到buf数组后面
- WriteByte将字节c添加到buf数组后边
- WriteRune将rune字符添加到buf数组后面
- WriteString将字符串添加到buf数组后面
写入方法
\(bytes.Buffer\) 也支持这四个写入方法。
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)

strings.Builderorganizes the content based on the internal slice to organize. When you call write-methods, they append new bytes to inner-slice. If the slice’s capacity is reached, Go will allocate a new slice with different memory space and copy old slice to a new one. It will take resource to do when the slice is large or it may create the memory issueThe
runeand a character ofstringcan be more than 1 bytes when youWriteRune()orWriteString()
我们可以预定义切片的容量来避免重新分配。
扩容方法
追加内容也有讲究,因为底层是 \(slice\),追加数据时有可能引起 \(slice\) 扩容。一般的优化方案是为 \(slice\) 初始化合理的空间,避免多次扩容复制。\(Builder\) 也提供了预分配内存的方法,如 \(Grow\) 方法。
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
注意扩容的容量和 \(slice\) 直接扩容两倍的方式略有不同,它是2*cap(b.buf)+n,之前容量的两倍加 \(n\)。
- 如果容量是10,长度是5,调用 \(Grow(3)\)结果是什么?当前容量足够使用,没有任何操作;
- 如果容量是10,长度是5,调用 \(Grow(7)\)结果是什么?剩余空间是5,不满足7个扩容空间,底层需要扩容。扩容的时候按照之前容量的两倍再加 \(n\) 的新容量扩容,结果是 \(2*10+7=27\)。
String() 方法
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
先获取 \([\ ]byte\) 地址,然后转成字符串指针,然后再取地址。
从 ptype 输出的结构来看,string 可看做 [2]uintptr,而 [ ]byte 则是 [3]uintptr,这便于我们编写代码,无需额外定义结构类型。如此,str2bytes 只需构建 [3]uintptr{ptr, len, len},而 bytes2str 更简单,直接转换指针类型,忽略掉 cap 即可。
禁止复制
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
\(Builder\) 的底层数据,它还有个字段 \(addr\) ,是一个指向 \(Builder\) 的指针。默认情况是它会指向自己。
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
\(copyCheck\) 用来保证复制后不允许修改的逻辑。仔细看下源码,如果 \(addr\) 是空,也就是没有数据的时候是可以被复制后修改的,一旦那边有数据了,就不能这么搞了。在 \(Grow\)、\(Write\)、\(WriteByte\)、\(WriteString\)、\(WriteRune\) 这五个函数里都有这个检查逻辑。
var b1 strings.Builder
b2 := b1
b2.WriteString("DEF")
b1.WriteString("ABC")
// b1 = ABC, b2 = DEF
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF")
代码将会报错 illegal use of non-zero Builder copied by value
下面的意思是拷贝过来的Builder进行添加修改,会造成其他Builder的修改。
When we copy the Builder, we clone the pointer of the slice but they still point to the old array. The problem will be occurs when you try to
Writesomething to copied Builder or source Builder, the other’s content will be affects. That’s reason whystrings.Builderprevent copy actions.

我们可以使用 \(Reset\) 方法对 \(addr、buf\) 置空。下面拷贝了使用 \(Reset\) 后不会报错。
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.Len()) // 3
fmt.Println(b2.String()) // ABC
b2.Reset()
b2.WriteString("DEF")
fmt.Println(b2.String()) // DEF
线程不安全
func main() {
var b strings.Builder
var n int32
var wait sync.WaitGroup
for i := 0; i < 1000; i++ {
wait.Add(1)
go func() {
atomic.AddInt32(&n, 1)
b.WriteString("1")
wait.Done()
}()
}
wait.Wait()
fmt.Println(len(b.String()), n)
}
905 1000
结果是 \(905\ 1000\),并不都是 \(1000\) 。如果想保证线程安全,需要在 \(WriteString\) 的时候加锁。
io.Writer 接口
\(strings.Builder\) 实现了 \(io.Writer\) 接口。可以使用在很多例子中
- io.Copy(dst Writer, src Reader) (written int64, err error)
- bufio.NewWriter(w io.Writer) *Writer
- fmt.Fprint(w io.Writer, a …interface{}) (n int, err error)
- func (r *http.Request) Write(w io.Writer) error
- and other libraries that uses \(io.Writer\)
代码
func main() {
var b strings.Builder
fmt.Printf("%v", b)
fmt.Println(b.Len(), b.Cap())
for i := 3; i >= 1; i-- {
fmt.Fprintf(&b, "%d#", i)
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
}
b.WriteString("Hello")
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
fmt.Println(b.String())
// b.Grow(5) // ^ 扩容
b.Grow(88) // ^ 扩容
fmt.Printf("%v\n", b)
fmt.Println(b.Len(), b.Cap())
fmt.Println(unsafeEqual("Hello", []byte{72, 101, 108, 108, 111}))
}
func unsafeEqual(a string, b []byte) bool {
bbp := *(*string)(unsafe.Pointer(&b))
return a == bbp
}
{<nil> []}0 0
{0xc0001223a0 [51 35]}
2 8
{0xc0001223a0 [51 35 50 35]}
4 8
{0xc0001223a0 [51 35 50 35 49 35]}
6 8
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 16
3#2#1#Hello
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 120
true
strings.Join 源码解析
实现原理
// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
n += len(elems[i])
}
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}
前面计算出整个字符串需要的长度 \(n\),然后创建 \(strings.Builder\) 并通过 \(Grow\) 方法直接扩容,大小为 \(0*2+n\) 为 \(n\) ,然后通过 \(WriteString\) 方法写入,最后调用 \(String\) 方法返回字符串。只构造了一次字符串对象。
代码
func main() {
args := make([]string, 10000)
for i := 0; i < 10000; i++ {
args = append(args, strconv.Itoa(i+1))
}
var s string
start := time.Now()
for i := range args {
s += args[i]
s += " "
}
// fmt.Println(s) // ^ 因为切片数量过大先不打印就只做合并操作
t := time.Since(start)
fmt.Println("elapsed time", t)
/* -------------------------------------------------------------------------- */
start = time.Now()
s = strings.Join(args, " ")
fmt.Println(s)
t = time.Since(start)
fmt.Println("elapsed time", t)
}
elapsed time 230.8618ms
elapsed time 0s
Golang 源码解读 01、深入解析 strings.Builder、strings.Join的更多相关文章
- jvm源码解读--01 jvm加载java/lang/object过程
现在做一下记录,这个看了两天,看的过程发现了很多c++的高级特性,没接触过,还得慢慢撸,禁止很慢 那么现在开始 吧 先打两个断点 java.c:351 JavaMain(void * _args) { ...
- springboot源码解读01
package org.springframework.web; @javax.servlet.annotation.HandlesTypes({org.springframework.web.Web ...
- swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?
date: 2018-8-01 14:22:17title: swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?description: 阅读 sowft 框架源码, 了解 sowf ...
- Vue 源码解读(8)—— 编译器 之 解析(上)
特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)-- 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)-- 编译器 之 解析(下)一起阅读. 前言 V ...
- Vue 源码解读(8)—— 编译器 之 解析(下)
特殊说明 由于文章篇幅限制,所以将 Vue 源码解读(8)-- 编译器 之 解析 拆成了两篇文章,本篇是对 Vue 源码解读(8)-- 编译器 之 解析(上) 的一个补充,所以在阅读时请同时打开 Vu ...
- CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析
目录 1. 3DTiles 数据集的类型 2. 创建瓦片树 2.1. 请求入口文件 2.2. 创建树结构 2.3. 瓦片缓存机制带来的能力 3. 瓦片树的遍历更新 3.1. 三个大步骤 3.2. 遍历 ...
- AFNetworking 3.0 源码解读 总结(干货)(下)
承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...
- AFNetworking 3.0 源码解读(五)之 AFURLSessionManager
本篇是AFNetworking 3.0 源码解读的第五篇了. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager AFNetworking 3 ...
- Alamofire源码解读系列(一)之概述和使用
尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想 前言 因为之前写过一个AFNetworking的源码解读,所以就已经比较了解 ...
随机推荐
- 机器学习之BP神经网络
import random import math #神经元的定义 class Neuron: def __init__(self,bias): self.bias = bias self.weigh ...
- 一步一步迁移ASP.NET Core 6.0-Part2
.NET 6 发布后,我们现有的应用会逐步升级到这个版本,首当其冲的是原因的ASP.NET Core的工程,如果一步一步升级到ASP.NET Core 6.0. 上一篇博文中,跟大家详细介绍了中间件. ...
- openEuler网络配置+换源+桌面环境ukui等基本环境部署
镜像下载.域名解析.时间同步请点击阿里云开源镜像站 1.网络配置 你可以选择查看官方文档进行配置:配置网络 (openeuler.org) 接下来的操作基本都需要root权限,所以直接使用root用户 ...
- 钓鱼+DNS欺骗学习笔记
钓鱼+DNS欺骗学习笔记 0x00 写在前面 原文链接: http://www.cnblogs.com/hkleak/p/5186523.html 感谢大佬无私教学 0x01 步骤如下 第一步:布置钓 ...
- [happyctf]部分writeup
题目名称:sqltest所属:MISC考察点:盲注 眼力 耐心(好吧是废话) 附件下载下来 ,到手一个流量包,用wireshark打开,大致浏览了一下,抓的应该是盲注的数据流量. 这里有一个经验问题, ...
- 台式机ATX电源:各接口定义、启动方法、电源特点
ATX,英文全称:Advanced Technology Extended,是一种由Intel公司在1995年公布的PC机主板结构规范. ATX电源作用是把交流220V的电源转换为计算机内部使用的直流 ...
- 洛谷P1049 [NOIP2001 普及组] 装箱问题
本题就是一个简单的01背包问题 1.因为每个物品只能选一次,而且要使箱子的剩余空间为最小.所以可以确定属性为 MAX 2.由于是从n个物品里面选i个物品 那么就是选出的i个物品的空间总和要尽可能的 ...
- Mybatis中的分页
Mybatis中有哪些分页方式? 数组分页:查询出全部数据,然后再list中截取需要的部分.(逻辑分页) 优点:效率高 缺点:占用内存比较高 sql分页:只从数据库中查询当前页的数据.(物理分 ...
- vue开发chrome扩展,数据通过storage对象获取
开发chrome插件时遇到一个问题,那就是单文件组件的data数据需要从chrome提供的storage对象中获取,但是 chrome.storage.sync.get 方法是异步获取数据的,需要通过 ...
- Spring Bean生命周期回调方法
参阅官方文档:https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory ...