原文链接:Go语言如何高效的进行字符串拼接(6种形式进行比照剖析)
前言
哈喽,大家好,我是
asong
日常业务开发中离不开字符串的拼接操作,不同语言的字符串实现形式都不同,在
Go
语言中就提供了6种形式进行字符串拼接,那这几种拼接形式该如何抉择呢?应用那个更高效呢?本文咱们就一起来剖析一下。本文应用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.Pointer len int}
stringStruct
和slice
还是很类似的,str
指针指向的是某个数组的首地址,len
代表的就是数组长度。怎么和slice
这么类似,底层指向的也是数组,是什么数组呢?咱们看看他在实例化时调用的办法:
//go:nosplitfunc 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 strings += "asong"s += "真帅"
这种形式应用起来最简略,根本所有语言都有提供这种形式,应用+
操作符进行拼接时,会对字符串进行遍历,计算并开拓一个新的空间来存储原来的两个字符串。
字符串格式化函数fmt.Sprintf
Go
语言中默认应用函数fmt.Sprintf
进行字符串格式化,所以也可应用这种形式进行字符串拼接:
str := "asong"str = fmt.Sprintf("%s%s", str, str)
fmt.Sprintf
实现原理次要是应用到了反射,具体源码剖析因为篇幅的起因就不在这里详细分析了,看到反射,就会产生性能的损耗,你们懂得!!!
Strings.builder
Go
语言提供了一个专门操作字符串的库strings
,应用strings.Builder
能够进行字符串拼接,提供了writeString
办法拼接字符串,应用形式如下:
var builder strings.Builderbuilder.WriteString("asong")builder.String()
strings.builder
的实现原理很简略,构造如下:
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []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("asong")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 = opInvalid m, 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{"asong", "真帅"}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 Builder b.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 = "asong"buf = append(buf, base...)string(base)
如果想缩小内存调配,在将[]byte
转换为string
类型时能够思考应用强制转换。
Benchmark比照
下面咱们总共提供了6种办法,原理咱们根本晓得了,那么咱们就应用Go
语言中的Benchmark
来剖析一下到底哪种字符串拼接形式更高效。咱们次要分两种状况进行剖析:
- 大量字符串拼接
- 大量字符串拼接
因为代码量有点多,上面只贴出剖析后果,具体代码曾经上传github
:https://github.com/asong2020/...
咱们先定义一个根底字符串:
var base = "123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"
大量字符串拼接的测试咱们就采纳拼接一次的形式验证,base拼接base,因而得出benckmark后果:
goos: darwingoarch: amd64pkg: asong.cloud/Golang_Dream/code_demo/string_join/oncecpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHzBenchmarkSumString-16 21338802 49.19 ns/op 128 B/op 1 allocs/opBenchmarkSprintfString-16 7887808 140.5 ns/op 160 B/op 3 allocs/opBenchmarkBuilderString-16 27084855 41.39 ns/op 128 B/op 1 allocs/opBenchmarkBytesBuffString-16 9546277 126.0 ns/op 384 B/op 3 allocs/opBenchmarkJoinstring-16 24617538 48.21 ns/op 128 B/op 1 allocs/opBenchmarkByteSliceString-16 10347416 112.7 ns/op 320 B/op 3 allocs/opPASSok asong.cloud/Golang_Dream/code_demo/string_join/once 8.412s
大量字符串拼接的测试咱们先构建一个长度为200的字符串切片:
var baseSlice []stringfor i := 0; i < 200; i++ { baseSlice = append(baseSlice, base)}
而后遍历这个切片一直的进行拼接,因为能够得出benchmark
:
goos: darwingoarch: amd64pkg: asong.cloud/Golang_Dream/code_demo/string_join/muliticpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHzBenchmarkSumString-16 7396 163612 ns/op 1277713 B/op 199 allocs/opBenchmarkSprintfString-16 5946 202230 ns/op 1288552 B/op 600 allocs/opBenchmarkBuilderString-16 262525 4638 ns/op 40960 B/op 1 allocs/opBenchmarkBytesBufferString-16 183492 6568 ns/op 44736 B/op 9 allocs/opBenchmarkJoinstring-16 398923 3035 ns/op 12288 B/op 1 allocs/opBenchmarkByteSliceString-16 144554 8205 ns/op 60736 B/op 15 allocs/opPASSok asong.cloud/Golang_Dream/code_demo/string_join/muliti 10.699s
论断
通过两次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
都不会错,然而在大量字符串拼接时,间接+
也就是更优的形式,具体业务场景具体分析,不要一概而论。
文中代码已上传github
:https://github.com/asong2020/...
好啦,本文到这里就完结了,我是asong
,咱们下期见。
欢送关注公众号:Golang梦工厂