如何高效地拼接字符串

开发 后端
不久前,因为一些原因,我们决定用 Go 语言对一个 Java 项目进行重构。这个项目的业务非常简单,在实现简单业务的简单功能时,需要将几组短字符串按顺序拼接成一个长字符串。

[[402519]]

本文转载自微信公众号「架构技术漫谈」,作者橙子。转载本文请联系架构技术漫谈公众号。

不久前,因为一些原因,我们决定用 Go 语言对一个 Java 项目进行重构。这个项目的业务非常简单,在实现简单业务的简单功能时,需要将几组短字符串按顺序拼接成一个长字符串。毋庸置疑,使用 + 操作符,是常用的字符串拼接方法,这在很多编程语言中都适用,Go 也不例外。

功能重构很快完成了,但在代码 review 环节时,对新语言的好奇心频频冒出,Go 语言中是否有其他更为高级或者灵活的方法呢?经过一番调研尝试,初步得出使用 strings.Builder 是性能最优的结论,于是,决定用它替换 + 操作符,并部署到线上。

几个月后,该项目在原基础上需求有所增加。再次面对该代码,心情也发生了变化,使用 strings.Builder 固然可行,但它需要三行代码,相比之下,用 + 操作符一行代码便能实现。在简洁性和高效性之间,该如何抉择呢?Go 语言中,是否有鱼和熊掌兼得的方法?

抱着这样的心思,我总结出 Go 语言中至少有 6 种关于字符串的拼接方式。但新问题也随之产生了,为何 Go 语言支持如此多种拼接方式?每种方式的存在,其背后的原因和逻辑又是什么呢?

让我们先分两种常用场景、两种字符串长度进行对比看看。

一、不同情景下的效率测试

待拼接字符串长度、次数已知,可一次完成字符串拼接,测试结果如下:

32 字节以下。

超过 32 字节,未超过 64 字节。+ 操作符发生了一次内存分配,但效率依然很高。bytes.Buffer 高于 strings.Builder。

64 字节以上。+ 操作符优势依然明显,strings.Join() 也不甘示弱。

待拼接字符串长度、次数未知,需要循环追加完成拼接操作,测试结果如下:

32 字节以下。+ 每次拼接都会生成新字符串,导致大量的字符串创建、替代。

超过 32 字节,未超过 64 字节。bytes.Buffer 发生 2 次内存分配。

64 字节以上。bytes.Buffer 优势已然不再, strings.Builder 一骑绝尘。不过,这似乎还不是最终的结果。

大量字符串拼接,终于要使出 strings.Builder 的必杀器 Grow() 了,bytes.Buffer 也有 Grow() 方法,但似乎作用不大。

二、原理分析

从上面的测试结果可以看出,在不同情况下,每种拼接方式的效率都不同,为什么会这样呢?那就得从它们的拼接原理说起。

+ 操作符,也叫级联符。它使用简单、应用广泛。

  1. res := "发" + "发" 

拼接过程:

1.编译器将字符串转换成字符数组后调用 runtime/string.go 的 concatstrings() 函数

2.在函数内遍历字符数组,得到总长度

3.如果字符数组总长度未超过预留 buf(32字节),使用预留,反之,生成新的字符数组,根据总长度一次性分配内存空间

4.将字符串逐个拷贝到新数组,并销毁旧数组

+= 追加操作符,与 + 操作符相同,也是通过 runtime/string.go的concatstrings() 函数实现拼接,区别是它通常用于循环中往字符串末尾追加,每追加一次,生成一个新的字符串替代旧的,效率极低。

  1. res := "发" 
  2.   res += "发" 

拼接过程:

1.同上

bytes.Buffer ,在 Golang 1.10 之前,它是循环中往末尾追加效率最高的方法,尤其是当拼接的字符串数量较大时。

  1. var b bytes.Buffer    // 创建一个 buffer 
  2. b.WriteString("发")   // 将字符串追加到buffer上  
  3. b.WriteString("发")    
  4. b.String()            // 取出字符串并返回 

拼接过程:

1.创建 []byte ,用于缓存需要拼接的字符串

2.首次使用 WriteString() 填充字符串时,由于字节数组容量为 0 ,最少会发生 1 次内存分配

3.待拼接字符串长度小于 64 字节,make 一个长度为字符串总长度,容量为 64 字节的新数组

4.待拼接字符串超过 64 字节时动态扩容,按 2* 当前容量 + 待拼接字符长度 make 新字节数组

5.将字节数组转换成 string 类型返回

strings.Builder 在 Golang 1.10 更新后,替代了byte.Buffer,成为号称效率最高的拼接方法。

  1. var b strings.Builder   
  2.  b.WriteString("发")    
  3.  b.WriteString("发"
  4.  b.String() 

拼接过程:

1.创建 []byte,用于缓存需要拼接的字符串

2.通过 append 将数据填充到前面创建的 []byte 中

3.append 时,如果字符串超过初始容量 8 且小于 1024 字节时,按乘以 2 的容量创建新的字节数组,超过 1024 字节时,按 1/4 增加

4.将老数据复制到新创建的字节数组中 5.追加新数据并返回

strings.Join() 主要适用于以指定分隔符方式连接成一个新字符串,分隔符可以为空,在字符串一次拼接操作中,性能仅次于 + 操作符。

  1. strings.Join([]string{"发""发"}, ""

拼接过程:

1.接收的是一个字符切片

2.遍历字符切片得到总长度,据此通过 builder.Grow 分配内存

3.底层使用了 strings.Builder,每使用一次 strings.Join() ,都会创建新的 builder 对象

fmt.Sprintf(),返回使用 format 格式化的参数。除了字符串拼接,函数内还有很多格式方面的判断,性能不高,但它可以拼接多种类型,字符串或数字等。

  1. fmt.Sprintf("str1 = %v,str2 = %v""发""发"

拼接过程:

1.创建对象

2.字符串格式化操作

3.将格式化后的字符串通过 append 方式放到[] byte 中

4.最后将字节数组转换成 string 返回

三、结论

在待拼接字符串确定,可一次完成字符串拼接的情况下,推荐使用 + 操作符,即便 strings.Builder 用 Grow() 方法预先扩容,其性能也是不如 + 操作符的,另外,Grow()也不可设置过大。

 

在拼接字符串不确定、需要循环追加字符串时,推荐使用 strings.Builder。但在使用时,必须使用 Grow() 预先扩容,否则性能不如 strings.Join()。

 

责任编辑:武晓燕 来源: 架构技术漫谈
相关推荐

2021-10-31 23:01:50

语言拼接字符串

2022-11-25 07:53:26

bash脚本字符串

2019-12-25 15:41:50

JavaScript程序员编程语言

2021-04-15 00:16:18

JavaString字符串

2011-07-11 16:00:22

字符串拼接

2021-06-11 18:08:00

Java字符串拼接

2013-06-24 15:16:29

Java字符串拼接

2010-10-09 11:43:10

MYSQL字符串

2023-10-31 18:57:02

Java字符串

2021-12-10 08:17:48

字符串拼接场景

2011-07-11 15:36:44

JavaScript

2024-09-06 17:32:55

字符串Python

2021-03-08 07:46:53

Git开源控制系统

2016-10-12 10:18:53

Java字符串源码分析

2023-12-11 08:39:14

Go语言字符串拼

2019-02-27 09:08:20

Java 8StringJoineIDEA

2011-07-18 13:34:44

SQL Server数拼接字符串

2022-05-11 09:51:10

云计算公共云

2021-08-05 18:34:55

IntelliJ ID高效

2013-04-28 10:36:00

Obj-C数组Obj-C字符串拼接与
点赞
收藏

51CTO技术栈公众号