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.xdnf.cn/news/142022.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【从0学习Solidity】45. 时间锁

【从0学习Solidity】45. 时间锁 博主简介&#xff1a;不写代码没饭吃&#xff0c;一名全栈领域的创作者&#xff0c;专注于研究互联网产品的解决方案和技术。熟悉云原生、微服务架构&#xff0c;分享一些项目实战经验以及前沿技术的见解。关注我们的主页&#xff0c;探索全栈开…

关于vantUI的导航组件tab标签页在ios和安卓中运用遇到的坑

vantTab的默认值 应用场景问题描述原始代码更正代码 应用场景 根据路由传值设置默认tab页&#xff0c;获取不同的数据并进行展示 问题描述 ios可正常按照路由传值默认tab页&#xff0c;安卓始终默认tabList的第一个value值&#xff0c;疑安卓系统中不接受dataMap.tabActive为…

YOLOv5如何训练自己的数据集(生活垃圾数据集为例)

文章目录 前言1、数据标注说明2、定义自己模型文件3、训练模型参考文献 前言 本文主要介绍如何利用YOLOv5训练自己的数据集 1、数据标注说明 以生活垃圾数据集为例子 生活垃圾数据集&#xff08;YOLO版&#xff09;点击这里直接下载本文生活垃圾数据集 生活垃圾数据集组成&…

Python开发与应用实验2 | Python基础语法应用

*本文是博主对学校专业课Python各种实验的再整理与详解&#xff0c;除了代码部分和解析部分&#xff0c;一些题目还增加了拓展部分&#xff08;⭐&#xff09;。拓展部分不是实验报告中原有的内容&#xff0c;而是博主本人自己的补充&#xff0c;以方便大家额外学习、参考。 &a…

【JavaEE】多线程案例-线程池

文章目录 1. 什么是线程池2. 为什么要使用线程池&#xff08;线程池有什么优点&#xff09;3. 如何使用Java标准库提供的线程池3.1 创建一个线程池对象3.2 什么是工厂模式3.3 为什么要使用工厂模式3.4 Executors 创建不同具有不同特性的线程池3.5 ThreadPool 类的构造方法3.6 线…

C++之list成员函数应用总结(二百三十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

Neo4j 与 Cypher 基础

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 简介 Neo4j 是用 Java 实现的开源 NoSQL 图数据库。从2003年开始开发&#xff0c;2007年正式发布第一版&#xff0c;其源码托管于 GitHub。 与常见的关系型数据库不同&#xff0c;Neo4j 基于图图结构来表示…

学习:原码-反码-补码

文章目录 前提知识原码详解反码补码 二进制负数的运算 前提知识 正数不需要进行原码反码补码一说&#xff0c;正数就是正数&#xff0c;我们原码反码补码是为了针对负数 &#xff08;按道理来说其实根本不存在什么码&#xff0c;只有二进制机器码&#xff0c;不过是为了方便计…

华为云云耀云服务器L实例评测|华为云上安装etcd

文章目录 华为云云耀云服务器L实例评测&#xff5c;华为云上安装etcd一、什么是etcd官方硬件建议 二、华为云主机准备三、etcd安装1. 安装预构建的二进制文件2. 从源代码构建 四、etcd服务注册与发现1. 配置etcd2. 使用systemctl 管理启动etcd服务3. 注册服务4. 发现服务 五、其…

ISE_ChipScope Pro的使用

1.ChipScope Pro Core Inserter 使用流程 在之前以及编译好的流水灯实验上进行学习 ChipScope的使用。 一、新建一个ChipScope 核 点击Next,然后在下一个框中选择 Finish&#xff0c;你就会在项目菜单中看到有XX.cdc核文件。 二、对核文件进行设置 右键“Synthesize – XST” …

最快的包管理器--pnpm创建vue项目完整步骤

1.用npm全局安装pnpm npm install -g pnpm 2.在要创建vue项目的包下进入cmd&#xff0c;输入&#xff1a; pnpm create vue 3.输入项目名字&#xff0c;选择Router,Pinia,ESLint,Prettier之后点确定 4.cd到创建好的项目 &#xff0c;安装依赖 cd .\刚创建好的项目名称\ p…

怎样快速打开github.com

1访问这个网站很慢是因为有DNS污染&#xff0c;被一些别有用心的人搞了鬼了&#xff0c; 2还有一个重要原因是不同的DNS服务器解析的速度不一样。 1 建议设置dns地址为114.114.114.114.我觉得假设一个县城如果有一个DNS服务器的话&#xff0c;这个服务器很可能不会存储…

[linux]服务器挂代理提升下载权重速度

写在前面 这里主要以huggingface下载权重为例&#xff0c;介绍如何在linux中部署代理提升下载速度 实际操作 第一步&#xff1a;服务器安装clash文件 https://github.com/Dreamacro/clash/releases#下载clash链接第二步&#xff1a;使用自己的配置文件 将config.yaml替换掉…

前端项目练习(练习-003-webpack-01)

学习webpack前&#xff0c;首先&#xff0c;创建一个web-003项目&#xff0c;内容和web-002一样。&#xff08;注意将package.json中的name改为web-003&#xff09; 想想&#xff0c;我们开发Java 的时候&#xff0c;Maven帮我们做的主要是编译&#xff0c;打包等等内容。开发前…

Spring Cloud Alibaba Gateway 简单使用

文章目录 Spring Cloud Alibaba Gateway1.Gateway简介2. 流量网关和服务网关的区别3. Spring Cloud Gateway 网关的搭建3.1 Spring Cloud Gateway 配置项的说明3.2 依赖导入3.3 配置文件 Spring Cloud Alibaba Gateway 1.Gateway简介 Spring Cloud Gateway是一个基于Spring F…

计算机竞赛 深度学习乳腺癌分类

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…

构建自己的物料解决方案——构建物料库,实现前端设计

01: 数据拦截简化数据获取流程 /** * 响应拦截器&#xff1a; * 服务端返回数据之后&#xff0c;前端 .then 之前被调用 */ service.interceptors.response.use((response) > {const { success, message, data } response.dataif (success) {return data}// TODO&#xff…

法规标准-UN R48标准解读

UN R48是做什么的&#xff1f; UN R48全名为关于安装照明和灯光标志装置的车辆认证的统一规定&#xff0c;主要描述了对各类灯具的布置要求及性能要求&#xff1b;其中涉及自动驾驶功能的仅有6.25章节【后方碰撞预警信号】&#xff0c;因此本文仅对此章节进行解读 功能要求 …

Python中的设计模式 -- 单例

迷途小书童 读完需要 2分钟 速读仅需 1 分钟 当我们谈到单例模式时&#xff0c;可以想象一个非常特殊的餐厅&#xff0c;这个餐厅只有一个桌子&#xff0c;无论多少人来用餐&#xff0c;都只能坐在这个桌子上。这个桌子就是餐厅的单例&#xff0c;它保证了整个餐厅中只有一个桌…

Element登录+注册

Element登录注册 1.1 定义1.3 完成用户注册登录界面搭建1.3.3 下载js依赖1.3.4 创建用户登录注册组件1.3.5 配置路由 二、数据交互2.1 数据导入2.3 安装引用相关模块 2.3.1 安装相关模块2.3.2 引用相关模块2.4 axios之get请求2.5 axios之post请求 四、注册 1.1 定义 ElementUI是…