Go语言6种字符串拼接的方式
Go语言6种字符串拼接的方式
- 前言
- string类型
- 字符串拼接的6种方式及原理
- 原生拼接方式"+"
- 字符串格式化函数fmt.Sprintf
- Strings.builder
- bytes.Buffer
- strings.join
- 切片append
- Benchmark对比
- 总结
前言
本文使用Go语言版本:1.17.1
string类型
我们首先来了解一下Go语言中string类型的结构定义,先来看一下官方定义:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
string是一个8位字节的集合,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil。string的值是不能改变的。
string类型本质也是一个结构体,定义如下:
type stringStruct struct {str unsafe.Pointerlen int
}
stringStruct和slice还是很相似的,str指针指向的是某个数组的首地址,len代表的就是数组长度。怎么和slice这么相似,底层指向的也是数组,是什么数组呢?我们看看他在实例化时调用的方法:
//go:nosplit
func gostringnocopy(str *byte) string {ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}s := *(*string)(unsafe.Pointer(&ss))return s
}
入参是一个byte类型的指针,从这我们可以看出string类型底层是一个byte类型的数组,所以我们可以画出这样一个图片:
string类型本质上就是一个byte类型的数组,在Go语言中string类型被设计为不可变的,不仅是在Go语言,其他语言中string类型也是被设计为不可变的,这样的好处就是:在并发场景下,我们可以在不加锁的控制下,多次使用同一字符串,在保证高效共享的情况下而不用担心安全问题。
string类型虽然是不能更改的,但是可以被替换,因为stringStruct中的str指针是可以改变的,只是指针指向的内容是不可以改变的,也就说每一个更改字符串,就需要重新分配一次内存,之前分配的空间会被gc回收。
关于string类型的知识点就描述这么多,方便我们后面分析字符串拼接。
字符串拼接的6种方式及原理
原生拼接方式"+"
Go语言原生支持使用+操作符直接对两个字符串进行拼接,使用例子如下:
var s string
s += "wz"
s += "真帅"
这种方式使用起来最简单,基本所有语言都有提供这种方式,使用+操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
字符串格式化函数fmt.Sprintf
Go语言中默认使用函数fmt.Sprintf进行字符串格式化,所以也可使用这种方式进行字符串拼接:
str := "wz"
str2 := "真帅"
str = fmt.Sprintf("%s%s", str, str2)
fmt.Sprintf实现原理主要是使用到了反射,具体源码分析因为篇幅的原因就不在这里详细分析了,看到反射,就会产生性能的损耗,你们懂得!!!
Strings.builder
Go语言提供了一个专门操作字符串的库strings,使用strings.Builder可以进行字符串拼接,提供了writeString方法拼接字符串,使用方式如下:
var builder strings.Builder
builder.WriteString("wz")
builder.String()
strings.builder的实现原理很简单,结构如下:
type Builder struct {addr *Builder // of receiver, to detect copies by valuebuf []byte // 1
}
addr字段主要是做copycheck,buf字段是一个byte类型的切片,这个就是用来存放字符串内容的,提供的writeString()方法就是像切片buf中追加数据:
func (b *Builder) WriteString(s string) (int, error) {b.copyCheck()b.buf = append(b.buf, s...)return len(s), nil
}
提供的String方法就是将[]]byte转换为string类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝:
func (b *Builder) String() string {return *(*string)(unsafe.Pointer(&b.buf))
}
bytes.Buffer
因为string类型底层就是一个byte数组,所以我们就可以Go语言的bytes.Buffer进行字符串拼接。bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte。使用方式如下:
buf := new(bytes.Buffer)
buf.WriteString("wz")
buf.String()
bytes.buffer底层也是一个[]byte切片,结构体如下:
type Buffer struct {buf []byte // contents are the bytes buf[off : len(buf)]off int // read at &buf[off], write at &buf[len(buf)]lastRead readOp // last read operation, so that Unread* can work correctly.
}
因为bytes.Buffer可以持续向Buffer尾部写入数据,从Buffer头部读取数据,所以off字段用来记录读取位置,再利用切片的cap特性来知道写入位置,这个不是本次的重点,重点看一下WriteString方法是如何拼接字符串的:
func (b *Buffer) WriteString(s string) (n int, err error) {b.lastRead = opInvalidm, ok := b.tryGrowByReslice(len(s))if !ok {m = b.grow(len(s))}return copy(b.buf[m:], s), nil
}
切片在创建时并不会申请内存块,只有在往里写数据时才会申请,首次申请的大小即为写入数据的大小。如果写入的数据小于64字节,则按64字节申请。采用动态扩展slice的机制,字符串追加采用copy的方式将追加的部分拷贝到尾部,copy是内置的拷贝函数,可以减少内存分配。
但是在将[]byte转换为string类型依旧使用了标准类型,所以会发生内存分配:
func (b *Buffer) String() string {if b == nil {// Special case, useful in debugging.return "<nil>"}return string(b.buf[b.off:])
}
strings.join
Strings.join方法可以将一个string类型的切片拼接成一个字符串,可以定义连接操作符,使用如下:
baseSlice := []string{"wz", "真帅"}
strings.Join(baseSlice, "")
strings.join也是基于strings.builder来实现的,代码如下:
func Join(elems []string, sep string) string {switch len(elems) {case 0:return ""case 1:return elems[0]}n := len(sep) * (len(elems) - 1)for i := 0; i < len(elems); i++ {n += len(elems[i])}var b Builderb.Grow(n)b.WriteString(elems[0])for _, s := range elems[1:] {b.WriteString(sep)b.WriteString(s)}return b.String()
}
唯一不同在于在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。
切片append
因为string类型底层也是byte类型数组,所以我们可以重新声明一个切片,使用append进行字符串拼接,使用方式如下:
buf := make([]byte, 0)
base = "wz"
buf = append(buf, base...)
string(base)
如果想减少内存分配,在将[]byte转换为string类型时可以考虑使用强制转换。
Benchmark对比
- 当进行少量字符串拼接时,直接使用+操作符进行拼接字符串,效率还是挺高的,但是当要拼接的字符串数量上来时,+操作符的性能就比较低了;
- 函数fmt.Sprintf还是不适合进行字符串拼接,无论拼接字符串数量多少,性能损耗都很大,还是老老实实做他的字符串格式化就好了;
- strings.Builder无论是少量字符串的拼接还是大量的字符串拼接,性能一直都能稳定,这也是为什么Go语言官方推荐使用strings.builder进行字符串拼接的原因,在使用strings.builder时最好使用Grow方法进行初步的容量分配,观察strings.join方法的benchmark就可以发现,因为使用了grow方法,提前分配好内存,在字符串拼接的过程中,不需要进行字符串的拷贝,也不需要分配新的内存,这样使用strings.builder性能最好,且内存消耗最小。
- bytes.Buffer方法性能是低于strings.builder的,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,不像strings.buidler这样直接将底层的 []byte 转换成了字符串类型返回,这就占用了更多的空间。
同步最后分析的结论:
无论什么情况下使用strings.builder进行字符串拼接都是最高效的,不过要主要使用方法,记得调用grow进行容量分配,才会高效。strings.join的性能约等于strings.builder,在已经字符串slice的时候可以使用,未知时不建议使用,构造切片也是有性能损耗的;如果进行少量的字符串拼接时,直接使用+操作符是最方便也是性能最高的,可以放弃strings.builder的使用。
综合对比性能排序:
strings.join` ≈ `strings.builder` > `bytes.buffer` > `[]byte`转换`string` > "+" > `fmt.sprintf
总结
本文我们针对6种字符串的拼接方式进行介绍,并通过benckmark对比了效率,无论什么时候使用strings.builder都不会错,但是在少量字符串拼接时,直接+也就是更优的方式,具体业务场景具体分析,不要一概而论。
Go语言6种字符串拼接的方式相关推荐
- Go语言中的字符串拼接方法介绍
本文介绍Go语言中的string类型.strings包和bytes.Buffer类型,介绍几种字符串拼接方法. 目录 string类型 strings包 strings.Builder类型 strin ...
- golang插入字符串_golang 几种字符串的连接方式
golang 几种字符串的连接方式 最近在做性能优化,有个函数里面的耗时特别长,看里面的操作大多是一些字符串拼接的操作,而字符串拼接在 golang 里面其实有很多种实现. 实现方法 1. 直接使用运 ...
- Go 语言中的字符串拼接
目录 1. 通过 + 号连接两个字符串 2. 使用 sprintf 函数 3. 使用 Join 函数 4. 使用 bytes.Buffer 的 WriteString 函数 5. 使用 buffer. ...
- Java 5种字符串拼接方式性能比较。
最近写一个东东,可能会考虑到字符串拼接,想了几种方法,但对性能未知,于是用Junit写了个单元测试. 代码如下: import java.util.ArrayList; import java.uti ...
- c++语言怎么实现字符串拼接,C++ string类和字符串的访问和拼接操作
C++ 增强了对字符串的支持,除了可以使用c中的字符串,还可以使用内置的数据类型string,string类处理字符串会翻遍很多,完全可以代替C语言中的char 数组和char 指针. 使用sting ...
- Go字符串拼接的方式与性能对比
一.Go中字符串的特殊之处 Go中的字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节).由于该编码对占用字节长度的不定性,Go ...
- java字符串拼接常用方式
方式一:+ "+",是java操作运算符比较常用的,也是简单直接的一种方式. String aa = "魅言倾馨";String bb = "子非我鱼 ...
- Python 字符串拼接的方式(去掉空格)
1.加号 2.逗号:使用逗号连接两个字符串,字符串之间会多出一个空格: 注:可以在print末尾加sep=''去掉空格 3.直接连接:Python独特的连接方式,无论中间有无空格,都不显示空格 示例: ...
- C语言学习笔记---字符串拼接函数 strcat() 和 strncat()
strcat()函数 strcat()函数主要用来拼接字符串,用于将一个字符串拼接到另一个字符串的后面.下面通过一个简单的例子来演示一下这个函数的使用方法. int main() {char s ...
最新文章
- 对比java_java集合对比
- 美多商城之用户中心(收货地址1)
- 【BZOJ】4032: [HEOI2015]最短不公共子串(LibreOJ #2123)
- java final 变量 回收_java入门教程-Java中final,finally,finalize三个关键字的区别
- 常考数据结构与算法:反转字符串
- Html 教程 (8)表单
- pythonenumapi_Python调用windows API实现屏幕截图
- Postman中几个body请求格式区别及使用说明
- Struts2文件上传的大小限制问题
- YBTOJ:方程的解(组合数学)(插板法)
- ArcGIS改变数据集或要素类的的坐标系(投影)
- 数据结构排序法之插入法
- CloudIDE插件在手,按时下班不愁
- Ubuntu设置局域网Windows共享文件Samba
- statusBar——状态栏
- JAVA大数据-Week4-DAY3
- java学不下去能学web安全吗,这半年学习 Web 安全的一点心得体会
- 知乎上线诺贝尔奖主题圆桌 让科普更加多元有趣
- VMware虚拟机无法连接外网怎么解决
- 华为设备 配置成为FTP服务器/客户端