当前位置: 首页 > 科技观察

如何高效拼接字符串

时间:2023-03-15 00:02:06 科技观察

本文转载自微信公众号《架构技术漫谈》,作者橙子。转载本文请联系建筑技术谈公众号。前段时间,由于某些原因,我们决定用Go重构一个Java项目。这个项目的业务很简单。在实现简单业务的简单功能时,需要将几组短字符串按顺序拼接成一个长字符串。不用说,使用+运算符是一种常用的字符串拼接方法,在很多编程语言中都适用,Go也不例外。功能重构很快就完成了,但在代码审查环节,对新语言的好奇心频频涌现。Go语言还有其他更高级或者更灵活的方法吗?经过一些研究和尝试,初步得出结论,使用strings.Builder是性能最好的结论,所以决定用它来代替+运算符,部署上线。几个月后,该项目的需求量在原来的基础上有所增加。再看这段代码,心情就变了,用strings.Builder当然是可以的,但是需要三行代码,相比之下用+运算符需要一行代码。简洁与高效如何取舍?在Go语言中,有没有办法兼得呢?考虑到这一点,我得出结论,Go语言中至少有6种字符串连接方式。但新的问题也出现了。为什么Go语言支持这么多的拼接方式?每种方法存在的原因和逻辑是什么?我们先把两种常见的场景和两种字符串长度进行比较看看。1、不同场景下的效率测试已知需要拼接的字符串的长度和数量,一次性完成字符串拼接。测试结果如下:小于32字节。大于32字节,不大于64字节。+运算符进行内存分配,但仍然非常有效。bytes.Buffer高于strings.Builder。64字节或更多。+运算符的优势还是很明显的,strings.Join()也不甘示弱。待拼接字符串的长度和次数未知,需要循环追加,完成拼接操作。测试结果如下:小于32字节。+每次连接都会产生一个新的字符串,导致大量的字符串创建和替换。大于32字节,不大于64字节。bytes.Buffer2内存分配发生。64字节或更多。bytes.Buffer的优点没有了,strings.Builder是最好的。然而,这似乎并不是最终结果。大量的字符串串接起来,终于用到了strings.Builder的杀手级工具Grow()。bytes.Buffer也有一个Grow()方法,不过好像作用不大。2.原理分析从上面的测试结果可以看出,在不同的情况下,每种拼接方式的效率是不同的。为什么会这样?那我们就得从它们的拼接原理说起。+运算符也称为串联运算符。它使用方便,应用广泛。res:="发"+"发"拼接过程:1.编译器将字符串转为字符数组,调用runtime/string.go的concatstrings()函数2.在函数中遍历字符数组,得到总length3.如果字符数组的总长度不超过保留的buf(32字节),则使用保留的,否则生成一个新的字符数组,并根据总长度一次性分配内存空间4.复制字符串一个一个地添加到新数组,并销毁旧数组数组+=追加运算符与+运算符相同。也是通过runtime/string.go的concatstrings()函数拼接的。区别在于它通常用于在循环中追加到字符串的末尾。每次追加都会生成一个新的字符串替换旧的,效率极低。res:="发"res+="发"拼接过程:1.bytes.Buffer同上,在Golang1.10之前,它是最高效的追加到循环末尾的方式,尤其是当拼接的字符串数量较多时很大。varbbytes.Buffer//创建缓冲区b.WriteString("Send")//追加字符串到缓冲区b.WriteString("Send")b.String()//取出字符串返回拼接过程:1.创建[]byte,用于缓存需要拼接的字符串2.第一次使用WriteString()填充字符串时,由于字节数组的容量为0,所以至少会发生一次内存分配3.待拼接的字符串长度小于64字节,make一个新数组,其长度为字符串的总长度,容量为64字节。4.当待拼接的字符串超过64字节时,动态扩容,按照2*当前容量+待拼接的字符长度做一个新的字节数组。byte数组转成string类型返回给strings.Builder,Golang1.10更新后取代了byte.Buffer,成为号称最高效的拼接方式。varbstrings.Builderb.WriteString("send")b.WriteString("send")b.String()拼接过程:1、创建[]byte缓存需要拼接的字符串2。通过append向前面填充数据3.append在创建的[]byte中,如果字符串超过初始容量8,小于1024字节,则容量乘以2创建一个新的字节数组,当超过1024字节,将4增加1/4。将旧数据复制到新创建的字节数组中5.追加新数据并返回字符串。Join()主要适用于加入指定分隔符的新字符串。分隔符可以为空,一旦在字符串中进行串联操作,性能仅次于+运算符。strings.Join([]string{"发","发"},"")拼接过程:1.接收一个字符切片2.遍历字符切片得到总长度,通过builder分配内存。相应增长3.底层使用strings.Builder。每次使用strings.Join()时,都会创建一个新的构建器对象fmt.Sprintf()并返回使用format格式化的参数。函数中除了字符串拼接,还有很多格式判断,性能不高,但是可以拼接多种类型,比如字符串或者数字。fmt.Sprintf("str1=%v,str2=%v","send","send")拼接过程:1.创建对象2.字符串格式化操作3.将格式化后的字符串追加到[]byte中4.最后将byte数组转成字符串,返回3。结语当确定要拼接的字符串,可以一次性完成字符串拼接时,建议使用+运算符,即使strings.Builder使用Grow()方法是预扩展的,其性能不如+运算符。另外,Grow()不能设置太大。当拼接的字符串不确定,需要循环追加字符串时,推荐使用strings.Builder。但是在使用的时候,必须使用Grow()进行预展开,否则性能不如strings.Join()。