原文链接: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}

stringStructslice还是很类似的,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字段次要是做copycheckbuf字段是一个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.joinstrings.builder > bytes.buffer > []byte转换string > "+" > fmt.sprintf

总结

本文咱们针对6种字符串的拼接形式进行介绍,并通过benckmark比照了效率,无论什么时候应用strings.builder都不会错,然而在大量字符串拼接时,间接+也就是更优的形式,具体业务场景具体分析,不要一概而论。

文中代码已上传github:https://github.com/asong2020/...

好啦,本文到这里就完结了,我是asong,咱们下期见。

欢送关注公众号:Golang梦工厂