go字符串拼接方式及性能比拼

在golang中字符串的拼接方式有多种,本文将会介绍比较常用的几种方式,并且对各种方式进行压测,以此来得到在不同场景下更适合使用的方案。

文章目录

    • 1、go字符串的几种拼接方式
      • 1.1 `fmt.Sprintf`
      • 1.2 `+运算符拼接`
      • 1.3 `strings.Join`
      • 1.4 `strings.Builder`
      • 1.5 `bytes.Buffer`
    • 2、性能测试
    • 3、源码分析
      • 3.1 +拼接
      • 3.2 strings.Builder
      • 3.3 strings.Join
      • 3.4 bytes.Buffer
      • 3.5 fmt.Sprintf
    • 总结:
    • 压测代码:

1、go字符串的几种拼接方式

比如对于三个字符串,s1、s2、s3,需要将其拼接为一个字符串,有如下的几种方式:

1.1 fmt.Sprintf

s := fmt.Sprintf("%s%s%s", s1, s2, s3)

1.2 +运算符拼接

s := s1 + s2 + s3

1.3 strings.Join

s := strings.Join([]string{s1, s2, s3}, "")

1.4 strings.Builder

builder := strings.Builder{}
builder.WriteString(s1)
builder.WriteString(s2)
builder.WriteString(s3)
s := builder.String()

1.5 bytes.Buffer

buffer := bytes.Buffer{}
buffer.WriteString(s1)
buffer.WriteString(s2)
buffer.WriteString(s3)
s := buffer.String()

 

2、性能测试

上面介绍了5种字符串的拼接方式,那么它们的性能如何呢,接下来将对这五种字符串拼接进行一个性能测试:

go版本:go1.21.0

如下为性能测试的结果,代码将在最后面给出,总共有八种,分别为:

1.fmt.Sprintf

2.+

2.使用for循环和+拼接

4.strings.join

5.strings.Builder

6.strings.Builder(先使用Grow扩容)

7.bytes.Buffer

8.bytes.Buffer(先使用Grow扩容)

 

性能测试的结果如下(仅供参考):

拼接的字符串数量:3, 字符串长度:10, 性能如下

在这里插入图片描述

当字符串数量和长度较小时,性能从高到低:

+拼接 > strings.Builder(先Grow) > strings.Join > bytes.Buffer > bytes.Buffer(先Grow) > strings.Builder > +拼接(使用for循环) > fmt.Sprintf

 

拼接的字符串数量:5, 字符串长度:128, 性能如下

在这里插入图片描述

当字符串数量较多和长度较大时,性能从高到低:

strings.Builder(先Grow) > +拼接 > strings.Join > bytes.Buffer(先Grow) > fmt.Sprintf > strings.Builder > +拼接(使用for循环) > bytes.Buffer

从上面的压测来看,直接使用+拼接字符串和使用strings.Builder(需要先grow)以及使用strings.Join的性能都是不错的。
上面有几个重点需要关注的点:

1. 当字符串数量较少长度较小时,使用+来拼接字符串的效率非常高并且内存分配次数为0(栈内存分配)
2. 当字符串数量较少长度较小时,bytes.Grow使用和不使用区别不大 (bytes.Buffer的最小扩容容量为64)
3. fmt.Sprintf的内存分配次数最多(涉及大量的interface{}操作,导致逃逸)

接下来将从源码的角度来分析它们的性能

3、源码分析

注意:go的版本为1.21.0

3.1 +拼接

       如果从感觉上来讲,我们通常会认为使用+来拼接字符串肯定是最低效的,因为会有多次字符串的拷贝,结果不然,接下来从源码的角度进行分析,看为什么使用+来拼接字符串的效率是非常高的:

源码位于runtime/string.go下:

concatstrings实现了go的字符串+拼接,所有的字符串会被放入一个字符串切片中,并且会传入一个大小为32字节的字符数组。

如果拼接后的字符串长度较小并且不会发生逃逸,那么就会在栈上创建出大小为32字节的字符数组。

步骤如下:

  1. 首先计算拼接后的字符串的长度;
  2. 如果编译器可以确定拼接后的字符串不会发生逃逸,buf就不为nil,如果buf不为nil并且buf可以存放下拼接后的字符串,就使用buf
  3. 如果buf为nil或者大小不足,则会在堆上申请出一片可以存放下拼接后的字符串的空间,然后将字符串一个一个拷贝过去
// The constant is known to the compiler.
// There is no fundamental theory behind this number.
const tmpStringBufSize = 32type tmpBuf [tmpStringBufSize]byte// concatstrings implements a Go string concatenation x+y+z+...
func concatstrings(buf *tmpBuf, a []string) string {// 首先计算出拼接后的字符串的长度idx := 0l := 0count := 0for i, x := range a {n := len(x)if n == 0 {continue}if l+n < l {throw("string concatenation too long")}l += ncount++idx = i}	if count == 0 {return ""}// 如果只有一个字符串并且它不在栈上或者我们的结果没有转义调用帧(但是f != nil),那么我们可以直接返回该字符串。if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {return a[idx]}s, b := rawstringtmp(buf, l)for _, x := range a {copy(b, x)b = b[len(x):]}return s
}func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {// 如果buf不为nil而且buf可以存放下拼接后的字符串,就直接使用bufif buf != nil && l <= len(buf) {b = buf[:l]s = slicebytetostringtmp(&b[0], len(b))} else {// 否则在堆上分配一片区域s, b = rawstring(l)}return
}// 在堆上分配一片内存,并且返回底层字符串结构和切片结构,它们指向同一片内存
func rawstring(size int) (s string, b []byte) {p := mallocgc(uintptr(size), nil, false)return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}

通过上面的源码分析,可以得知,使用直接使用+拼接字符串会先申请出一片内存,然后将字符串一个一个拷贝过去,并且字符串有可能分配在栈上,因此效率非常高。

但是在使用for循环来拼接时,由于编译器无法确定最终的内存空间大小,因此会发生多次拷贝,效率很低。

当字符串比较小并且数量是已知的时,使用+拼接字符串的效率很高,并且代码可读性更好。

3.2 strings.Builder

除了使用+来拼接字符串,通常string.Builder使用的也是非常多的,并且它的效率相比也是更高的,接下来看一下Builder的实现

在Builder中有一个字节切片的buf,每次在写入时都会追加到buf中,当buf容量不足时,切片会自动扩容,但是在扩容时会拷贝旧的切片,因此如果预先使用Grow来分配内存,则可以减少扩容时的拷贝开销,从而提高效率。

另一个高效的原因是在使用String()获取字符串时直接共用了切片的底层存储数组,从而减少了一次数据的拷贝。因此Builder的所有api都是只能追加,不能修改的。

type Builder struct {addr *Builder // of receiver, to detect copies by valuebuf  []byte
}func (b *Builder) WriteString(s string) (int, error) {b.copyCheck()b.buf = append(b.buf, s...)return len(s), nil
}func (b *Builder) grow(n int) {buf := bytealg.MakeNoZero(2*cap(b.buf) + n)[:len(b.buf)]copy(buf, b.buf)b.buf = buf
}func (b *Builder) Grow(n int) {b.copyCheck()if n < 0 {panic("strings.Builder.Grow: negative count")}if cap(b.buf)-len(b.buf) < n {b.grow(n)}
}// 返回的string和buf共用了同一片底层字符数组,减少了数据拷贝
func (b *Builder) String() string {return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}

strings.Builder在获取字符串时返回的string和buf共用同一片字符数组,因此减少了一次数据拷贝。在使用时,使用grow预先分配内存可以减少切片扩容时的数据拷贝,提高性能,因此建议先使用Grow进行预分配

3.3 strings.Join

在上面的性能测试中,Join的性能也很高,因为strings.join本身使用了strings.Builder,并且在拼接字符串之前使用Grow进行了内存预分配,因此效率也很高。

代码很简单,就不再介绍。

3.4 bytes.Buffer

bytes.Buffer和strings.Builder比较相似,但是通常用于处理字节数据,而不是字符串。一个区别就是在使用String()方法来获取字符串时,有一次切片到字符串的拷贝,因此效率不如strings.Buffer
但是当字符串长度较小时,bytes.Buffer的效率甚至比strings.Buffer要高。是因为,Builder的扩容是按照切片的扩容策略来的,而Buffer的初始最小扩容大小为64,也就是第一次扩容最小大小为64,因此使用Grow和不使用的区别不大。

func (b *Buffer) String() string {if b == nil {// Special case, useful in debugging.return "<nil>"}return string(b.buf[b.off:])
}const smallBufferSize = 64func (b *Buffer) grow(n int) int {...if b.buf == nil && n <= smallBufferSize {b.buf = make([]byte, n, smallBufferSize)return 0}...
}

3.5 fmt.Sprintf

fmt.Sprintf的实现较为复杂,并且使用了大量的interface{},会导致内存逃逸,涉及到多次内存分配,效率较低。如果是纯字符串,通常不会使用fmt.Sprintf来进行拼接,fmt.Sprintf可以对多种数据格式进行字符串格式化。

总结:

1.当要拼接的多个字符串是已知并且数量较少时,可以直接使用+来拼接,效率比较高而且可读性更好

2、当要拼接的字符串数量和长度未知时,可以使用strings.Builder来拼接,并且预估字符串的大小使用Grow进行预分配,效率较高

3、当要拼接的字符串数量已知或者在拼接时需要加入分割字符串时,可以使用strings.Join,效率较高,也很方便

4、在进行字节数据处理时可以使用bytes.Buffer

5、当要对包含多种格式的数据进行字符串格式化时使用fmt.Sprintf,更加方便

 
 

压测代码:

package string_concatsimport ("bytes""fmt""math/rand""strings""testing""time"
)const dic = "qwertyuioplkjhgfdsazxcvbnmMNBVCXZASDFGHJKLPOIUYTREWQ0123456789"var defaultRand = rand.New(rand.NewSource(time.Now().UnixNano()))func RandString(n int) string {builder := strings.Builder{}builder.Grow(n)for i := 0; i < n; i++ {n := defaultRand.Intn(len(dic))builder.WriteByte(dic[n])}return builder.String()
}var (strs []stringN    = 5Len  = 128
)func init() {for i := 0; i < N; i++ {strs = append(strs, RandString(Len))}
}// fmt.Sprintf
func BenchmarkSprintf(b *testing.B) {for i := 0; i < b.N; i++ {_ = fmt.Sprintf("%s%s%s%s%s", strs[0], strs[1], strs[2], strs[3], strs[4])}
}// s1 + s2 + s3
func BenchmarkConcat(b *testing.B) {for i := 0; i < b.N; i++ {_ = strs[0] + strs[1] + strs[2] + strs[3] + strs[4]}
}// for循环+
func BenchmarkForConcat(b *testing.B) {for i := 0; i < b.N; i++ {var s stringfor i := 0; i < len(strs); i++ {s += strs[i]}}
}// strings.Join
func BenchmarkJoin(b *testing.B) {for i := 0; i < b.N; i++ {_ = strings.Join(strs, "")}
}// strings.Builder
func BenchmarkBuilder(b *testing.B) {for i := 0; i < b.N; i++ {builder := strings.Builder{}for i := 0; i < len(strs); i++ {builder.WriteString(strs[i])}_ = builder.String()}
}// strings.Builder
func BenchmarkBuilderGrowFirst(b *testing.B) {for i := 0; i < b.N; i++ {builder := strings.Builder{}n := 0for i := 0; i < len(strs); i++ {n += len(strs[i])}builder.Grow(n)for i := 0; i < len(strs); i++ {builder.WriteString(strs[i])}_ = builder.String()}
}// bytes.Buffer
func BenchmarkBuffer(b *testing.B) {for i := 0; i < b.N; i++ {buffer := bytes.Buffer{}for i := 0; i < len(strs); i++ {buffer.WriteString(strs[i])}_ = buffer.String()}
}// bytes.Buffer
func BenchmarkBufferGrowFirst(b *testing.B) {for i := 0; i < b.N; i++ {buffer := bytes.Buffer{}n := 0for i := 0; i < len(strs); i++ {n += len(strs[i])}buffer.Grow(n)for i := 0; i < len(strs); i++ {buffer.WriteString(strs[i])}_ = buffer.String()}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/145775.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LaTex的学习(学习于b站西北农林科技大学耿楠教授的教学视频)

目录 一、LaTeX软件的安装与环境配置  1.LaTeX软件texlive的下载  2. texlive的安装 二、用命令行实现LaTeX文档的编写  1.通过命令行演示LaTeX编写的过程  2.将编译LaTeX并生成pdf文件的过程封装成一个bat文件  3.演示一个含有中文的LaTeX文件 三、用TexStudio IDE实…

回归预测 | MATLAB实现RUN-XGBoost龙格库塔优化极限梯度提升树多输入回归预测

回归预测 | MATLAB实现RUN-XGBoost多输入回归预测 目录 回归预测 | MATLAB实现RUN-XGBoost多输入回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现RUN-XGBoost多输入回归预测&#xff08;完整源码和数据&#xff09; 1.龙格库塔优化XGBoost&#xff0c;…

【Redis】redis基本数据类型详解(String、List、Hash、Set、ZSet)

目录 RedisString(字符串)List(列表)Hash(字典)Set(集合)ZSet(有序集合) Redis Redis有5种基本的数据结构&#xff0c;分别为&#xff1a;string&#xff08;字符串&#xff09;、list&#xff08;列表&#xff09;、set&#xff08;集合&#xff09;、hash&#xff08;哈希&a…

ahk系列——ahk_v2实现win10任意界面ocr

前言&#xff1a; 不依赖外部api接口&#xff0c;界面简洁&#xff0c;翻译快速&#xff0c;操作简单&#xff0c; 有网络就能用 、还可以把ocr结果非中文翻译成中文、同样可以识别中英日韩等60多个国家语言并翻译成中文&#xff0c;十分的nice 1、所需环境 windows10及其以上…

Linux学习记录——삼십일 socket编程---TCP套接字

文章目录 TCP套接字简单通信1、服务端1、基本框架2、获取连接 2、客户端3、多进程4、多线程5、线程池6、简单的日志系统7、守护进程8、其它 TCP套接字简单通信 本篇gitee 学习完udp套接字通信后&#xff0c;再来看TCP套接字。 四个文件tcp_server.hpp&#xff0c; tcp_serve…

Linux常见操作命令(1)

​ 前言&#xff1a;作者也是初学Linux&#xff0c;可能总结的还不是很到位 ♈️今日夜电波&#xff1a;达尔文—林俊杰 0:30━━━━━━️&#x1f49f;──────── 4:06 &#x1f504; ◀️ …

Redis与分布式-分布式锁

接上文 Redis与分布式-集群搭建 1.分布式锁 为了解决上述问题&#xff0c;可以利用分布式锁来实现。 重新复制一份redis&#xff0c;配置文件都是刚下载时候的不用更改&#xff0c;然后启动redis服务和redis客户。 redis存在这样的命令&#xff1a;和set命令差不多&#xff0…

Windows上安装 Go 环境

一、下载go环境 下载go环境&#xff1a;Go下载官网链接找到自己想下载的版本&#xff0c;点击下载&#xff0c;比如我这是windows64位的&#xff0c;我就直接点击最新的。 二、安装go环境 双击下载的.msi文件 next next 他默认的是c盘&#xff0c;你自己可以改&#xff0c;然…

Redis优化

Redis优化 一、Sring数据类型1.1、 概述1.2、 set/get/append/strlen命令1.3、 incr/decr/incrby/decrby 命令1.4、 getset命令1.5、 setex命令1.6、 setnx命令1.7、 mset/mget/msetnx命令 二、List数据类型2.1、 概述2.2、 lpush/lpushx/lrange命令2.3、 lpop/llen命令2.4、 l…

phpstudy_pro高效率建一个属于自己的网站

1.下载phpStudy_32 2.下载wordpress-6.3-zh_CN 安装好phpstudy后启动phpstudy中对应的服务&#xff0c;并在网站中配置好对一个的应用的路径 ps:根目录中的路径是你想要通过phpstudy部署应用的路径 这里以wordpress为例 将下载wordpress的压缩包解压后&#xff0c;需要修改…

VS+Qt+C++ GDAL读取tif图像数据显示

程序示例精选 VSQtC GDAL读取tif图像数据显示 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《VSQtC GDAL读取tif图像数据显示》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;…

A. Sequence with Digits

题目&#xff1a;样例&#xff1a; 输入 8 1 4 487 1 487 2 487 3 487 4 487 5 487 6 487 7输出 42 487 519 528 544 564 588 628 思路&#xff1a; 暴力模拟题&#xff0c;看这数据范围&#xff0c;有些人可能会被唬住&#xff0c;以为是高精度或者容易超时&#xff0c;实际上…

Docker 自动化部署(实践)

常用命令 docker search jenkins查看需要的jenkins镜像源 docker pull jenkins/jenkins 拉取jenkins镜像 docker images查看下载的镜像源 docker ps 查看包含启动以及未启动的容器 docker ps -a查看启动的容器 docker rm 容器id/容器名称 删除容器 docker rm -f 容器id/容器名…

一站式企业协同研发云——云效

一站式企业协同研发云——云效 文章目录 一站式企业协同研发云——云效什么是云效云效的作用云效使用说明公司领导操作步骤项目创建者或项目组长操作步骤项目上线部署 什么是云效 云效是一种基于云计算技术的软件研发与交付管理平台&#xff0c;旨在提高团队的协作效率和软件交…

Redis与分布式-集群搭建

接上文 Redis与分布式-哨兵模式 1. 集群搭建 搭建简单的redis集群&#xff0c;创建6个配置&#xff0c;开启集群模式&#xff0c;将之前配置过的redis删除&#xff0c;重新复制6份 针对主节点redis 1&#xff0c;redis 2&#xff0c;redis 3都是以上修改内容&#xff0c;只是…

求各区域热门商品Top3 - HiveSQL

背景&#xff1a;这是尚硅谷SparkSQL练习题&#xff0c;本文用HiveSQL进行了实现。 数据集&#xff1a;用户点击表&#xff0c;商品表&#xff0c;城市表 题目: ① 求每个地区点击量前三的商品&#xff1b; ② 在①的基础上&#xff0c;求出每个地区点击量前三的商品后&a…

【Linux】【网络】传输层协议:TCP

文章目录 TCP 协议1. TCP 协议段格式2. TCP 报头解析3. TCP 的可靠性4. 面向字节流5. 粘包问题6. 连接队列维护 TCP 的 确认应答机制TCP 的 超时重传机制TCP 的 三次握手TCP 的 四次挥手setsockopt 函数&#xff1a;设置套接字选项&#xff0c;解决 TIME_WAIT 状态引起的 bind …

【Python】基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作

【Python】基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作 文章目录 手势识别人脸追踪键盘控制整体代码附录&#xff1a;列表的赋值类型和py打包列表赋值BUG复现代码改进优化总结 py打包 视频&#xff1a; 基于OpenCV人脸追踪、手势识别控制的求实之路FPS游戏操作 手…

小谈设计模式(7)—装饰模式

小谈设计模式&#xff08;7&#xff09;—装饰模式 专栏介绍专栏地址专栏介绍 装饰模式装饰模式角色Component&#xff08;抽象组件&#xff09;ConcreteComponent&#xff08;具体组件&#xff09;Decorator&#xff08;抽象装饰器&#xff09;ConcreteDecorator&#xff08;具…

玩转数据-大数据-Flink SQL 中的时间属性

一、说明 时间属性是大数据中的一个重要方面&#xff0c;像窗口&#xff08;在 Table API 和 SQL &#xff09;这种基于时间的操作&#xff0c;需要有时间信息。我们可以通过时间属性来更加灵活高效地处理数据&#xff0c;下面我们通过处理时间和事件时间来探讨一下Flink SQL …