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预分配足量的内存空间。

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
大勇前进1 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu1 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端
alsmile1 天前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白801 天前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端