Go语言中常见100问题-#38 字符串连接最佳实践

前言

在Go语言中,字符串连接主要有两种方法,其中一种在某些时候是非常低效的,通过本文学习我们应该掌握在不同的场景下选择最合适的方法。

字符串连接常规方法

下面的concat函数通过+=将一个字符串切片拼接成一个字符串。具体代码如下,在每轮循环中,通过+=操作符将切片中的字符串value拼接到字符串s中。咋看起来这段代码没有啥问题,但是我们不要忽略了一个重要原则:字符串是不可变的。因此每一轮迭代,不是直接更新s,而是在内存中重新分配一个字符串,这会很影响性能。

golang 复制代码
func concat(values []string) string {
    s := ""
    for _, value := range values {
        s += value
    }
    return s
}

优化方法

优化方法1:采用strings Builder

有什么优化方法吗?可以采用strings包提供的Builder结构体,实现如下。通过strings.Builder创建一个Builder结构体,在每次迭代中调用它的WriteString方法向里面的缓冲区buffer中追加value,减少内存的重新分配和拷贝。尽管WriteString方法第二返回值是一个error类型,但我们通常忽略它,因为该方法返回的error永远都是nil. 那为啥函数签名设计这样,搞一个error返回值呢?因为strings.Builder实现了io.StringWriter接口,该接口只有一个方法: WriteString(s string) (n int, err error),因此为实现该接口,所以strings.Builder的WriteString方法会返回一个error。

golang 复制代码
func concat(values []string) string {
    sb := strings.Builder{}
    for _, value := range values {
        _, _ = sb.WriteString(value)
    }
    return sb.String()
}

strings.Builder除了提供有WriteString方法,还有Write方法,写入一个字节切片,如果是写入单个字节,使用WriteByte方法,如果是写入一个rune字符,使用WriteRune方法。

内部实现上,strings.Builder含有一个字节切片,调用WriteString实际上是向内部的切片中append数据。有两点需要注意,一是strings.Builder不支持并发,内部同时调用append操作会导致数据竞争问题。二是需要设置内部切片的大小,否则如切片满了会重新分配空间,并拷贝原切片中的数据,导致效率低效。所以strings.Builder提供了一个对外方法Grow(n int)确保内部分配的空间有n个字节。

优化方法2:使用Grow

现在使用Grow方法对上面的代码进一步优化,一开始就设置好写入的所有字符串的字节数。实现代码如下。

golang 复制代码
func concat(values []string) string {
    total := 0
    for i := 0; i < len(values); i++ {
        total += len(values[i])
    }

    sb := strings.Builder{}
    sb.Grow(total)
    for _, value := range values {
        _, _ = sb.WriteString(value)
    }

    return sb.String()
}

三种方法性能测试对比

对上面三种字符串拼接实现做一个benckmark测试,输入的字符串切片包含1000个字符串,每个字符串长度为1000字节。测试结果如下:

console 复制代码
BenchmarkConcatV1-4 16 72291485 ns/op
BenchmarkConcatV2-4 1188 878962 ns/op
BenchmarkConcatV3-4 5922 190340 ns/op

可以看到,第3个版本比第1个版本快99%,比第2个版本快78%。也许有人会问,第三个版本不是对values进行两次迭代,但为啥它的效率比第二个版本还高呢?答案是没有预初始化切片大小,导致效率地下,在本系列文章Go语言中常见100问题-#21 切片初始化方法及最佳实践有详细分析。如果切片没有分配给定的容量,当切片不断append元素变满时,会导致额外的内存分配和数据拷贝。因此,采用两次迭代先统计占用的空间大小是值得的。

采用strings.Builder来拼接字符串是推荐的方法,这种方法建议在有循环的时候使用。如果没有循环,只是简单的个别字符串拼接,不推荐这种方法,因为性能提升并不明显,但使得代码的可读性比第一个版本差,直接使用+=或者fmt.Sprintf拼接即可。

思考总结

一般来说,从性能角度出发,我们需要记住如果拼接的字符串数量超过5个,采用strings.Builder进行拼接通常是比+=要快的。即使准确的数量(不一定是5)依赖于很多因素,像待拼接的字符串长度、运行的机器等。但这可以作为一个经验值在我们选择方法时提供一个参考。此外,如果我们预先知道拼接的字符串长度,应该使用Grow预分配足量的内存空间。

相关推荐
2401_857622661 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
2402_857589361 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没3 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式