1. 拼接字符串的方式
1.1 + 号拼接
因为 Go 中字符串不可更改的,所以 Go 底层每次拼接的时候要重新申请内存空间,将原来的字符串拷贝进去,然后再追加新的字符串。
func TestPlusConcat(t *testing.T) {
s1 := "FF14"
s2 := "World"
s3 := "FF15"
// go 底层会分配一个内存空间,然后把 s1 和 s2 的内容拷贝进去
newString := s1 + s2
// s1 拷贝了两次, s2 也是拷贝了两次, s3 拷贝了一次
s4 := newString + s3
fmt.Println(s3)
fmt.Println(s4)
}
1.2 字符串格式化函数 fmt.Sprintf
fmt.Sprintf
接受一个格式化字符串作为第一个参数,这个字符串包含了占位符(如 %d, %s, %f 等),这些占位符会被后续的参数替换。
func TestSprintfConcat(t *testing.T) {
userName := "Sakura"
website := "www.sakurasss.top"
email := "1808479176@qq.com"
newString := fmt.Sprintf("用户名:%s\\n网站:%s\\n邮箱:%s", userName, website, email)
fmt.Println(newString)
}
fmt.Sprintf
主要用于格式化字符串而不是拼接字符串
1.3 Strings.builder
Go 官方也提供了strings.Builder 包来操作字符串。我们采用和上面一样的操作方式,来拼接字符串。
func TestBuilderConcat(t *testing.T) {
var builder strings.Builder
// builder也可以采用 Grow 的方式提前分配内存,以减少内存的分配次数
//builder.Grow(1024)
builder.WriteString("FF14")
builder.WriteString("Sakura")
fmt.Println(builder.Len()) // 10
s := builder.String()
fmt.Println(s)
}
💡 另外,builder 是不允许进行拷贝的
func TestBuilderConcat(t *testing.T) {
var builder strings.Builder
builder.WriteString("Sakura")
builder2 := builder
builder2.WriteString("S")
fmt.Println()
}
panic: strings: illegal use of non-zero Builder copied by value [recovered]
panic: strings: illegal use of non-zero Builder copied by value
💡
strings.Builder
通过一个指针指向实际保存数据的底层 byte 数组,拷贝strings.Builder
时同时也拷贝了它的的指针, 但是拷贝过来的指针仍然指向之前的底层数组 (等于两者共享了一个底层数组) ,如果此时写入数据,那么被拷贝的strings.Builder
也会受到影响。
strings.builder
的实现原理很简单,结构如下:
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}
addr
字段主要是做 copycheck
,buf
字段是一个 byte
类型的切片,这个就是用来存放字符串内容的,提供的 writeString()
方法就是像切片 buf
中追加数据:
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
提供的 String
方法就是将 []]byte
转换为 string
类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝:
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
1.4 bytes.buffer
bytes.Buffer
底层是提供一个缓冲区,实现内容的操作。它的零值是一个随时可用的空字节缓冲区,通过以下方法操作缓冲区:
func (b *Buffer) Write(p []byte) (int, error)
func (b *Buffer) WriteByte(c byte) error
func (b *Buffer) WriteRune(r rune) (int, error)
func (b *Buffer) WriteString(s string) (int, error)
💡
strings.Builder
和bytes.Buffer
底层都是一个[]byte
,但是bytes.Buffer
转换字符串时重新申请了内存空间用来存放, 而strings.Builder
直接将底层的[]byte
转换为字符串返回。
另外,bytes.Buffer 的源代码中写到:
To build strings more efficiently, see the strings.Builder type. (构建字符串更高效的方法是 strings.Builder)
💡
bytes.buffer
实现了io.Writer
和io.Reader
接口,内部维护了一个切片 buf 以及两个重要的字段 off 和 ptr 来追踪读写位置。
func TestBufferConcat(t *testing.T) {
var buf bytes.Buffer
// 向 buffer 中写入数据
buf.WriteString("FF14")
buf.WriteString("World")
// buf.Reset() 用于清空 buffer,之后就读不到了
// 从 buffer 中读取数据
readDate := make([]byte, 4)
n, err := buf.Read(readDate)
if err != nil {
log.Println(err)
return
}
// 输出读取的数据
fmt.Println(string(readDate[:n]))
s := buf.String() // 获取 buffer 中的数据
fmt.Println(s) // 因为 FF14 已经被读取, 所以这里只会打印 World
}
1.5 strings.join
💡
Join
主要用于现成切片、数组的(毕竟拼接成数组也要时间)
func Join(elems []string, sep string) string
func BenchmarkPlusConcat2(b *testing.B) {
baseSlice := []string{"FF14", "World", "FF15"}
for i := 0; i < b.N; i++ {
strings.Join(baseSlice, "Sakura")
}
}
strings.join
也是基于 strings.builder
来实现的,内部调用了 b.Grow(n)
方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的 slice 的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
var n int
if len(sep) > 0 {
if len(sep) >= maxInt/(len(elems)-1) {
panic("strings: Join output length overflow")
}
n += len(sep) * (len(elems) - 1)
}
for _, elem := range elems {
if len(elem) > maxInt-n {
panic("strings: Join output length overflow")
}
n += len(elem)
}
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}
1.5 切片 append
func byteConcat(n int, str string) string {
buf := make([]byte, 0)
// 如果长度是可预知的,那么创建 []byte 时,我们还可以预分配切片的容量(cap)。
for i := 0; i < n; i++ {
buf = append(buf, str...)
}
return string(buf)
}
2. 性能测试
go test -bench=Conacat -v -benchmem
const letterBytes = "SakurasssFF14246810GoFlutter"
// 生成随机字符串
func randomString(n int) string {
// n 表示生成多长的字符串
b := make([]byte, n)
for i := range b {
// 每次循环随机中间随机一个字符,构成字符串
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func plusConcat(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}
func sprintfConcat(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s = fmt.Sprintf("%s%s", s, str)
}
return s
}
func joinConcat(n int, str []string) string {
newStr := ""
for i := 0; i < n; i++ {
newStr = strings.Join(str, "")
}
return newStr
}
func builderConcat(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
func bufferConcat(n int, s string) string {
buf := new(bytes.Buffer)
for i := 0; i < n; i++ {
buf.WriteString(s)
}
return buf.String()
}
func byteConcat(n int, str string) string {
buf := make([]byte, 0)
for i := 0; i < n; i++ {
buf = append(buf, str...)
}
return string(buf)
}
func preByteConcat(n int, str string) string {
buf := make([]byte, 0, n*len(str))
for i := 0; i < n; i++ {
buf = append(buf, str...)
}
return string(buf)
}
func BenchmarkPlusConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
plusConcat(10000, s)
}
}
func BenchmarkSprintfConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sprintfConcat(10000, s)
}
}
func BenchmarkBuilderConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
builderConcat(10000, s)
}
}
func BenchmarkJoinConcat(b *testing.B) {
s := randomString(10)
strs := []string{s}
b.ResetTimer()
for i := 0; i < b.N; i++ {
joinConcat(10000, strs)
}
}
func BenchmarkBufferConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
bufferConcat(10000, s)
}
}
func BenchmarkByteConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
byteConcat(10000, s)
}
}
func BenchmarkPreByteConcat(b *testing.B) {
s := randomString(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
preByteConcat(10000, s)
}
}
本文引用:
Go 高性能之字符串拼接 | Go 语言必知必会 (dbwu.tech) Go 字符串拼接6种,最快的方式 -- strings.builder - 技术颜良 - 博客园 (cnblogs.com) 字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔 (geektutu.com)
评论区