溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Go語(yǔ)言怎么高效的進(jìn)行字符串拼接

發(fā)布時(shí)間:2022-08-23 11:10:03 來(lái)源:億速云 閱讀:163 作者:iii 欄目:開(kāi)發(fā)技術(shù)

本篇內(nèi)容主要講解“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”吧!

    string類(lèi)型

    我們首先來(lái)了解一下Go語(yǔ)言中string類(lèi)型的結(jié)構(gòu)定義,先來(lái)看一下官方定義:

    // 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是一個(gè)8位字節(jié)的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但是不能為nil。string的值是不能改變的。

    string類(lèi)型本質(zhì)也是一個(gè)結(jié)構(gòu)體,定義如下:

    type stringStruct struct {
        str unsafe.Pointer
        len int
    }

    stringStructslice還是很相似的,str指針指向的是某個(gè)數(shù)組的首地址,len代表的就是數(shù)組長(zhǎng)度。怎么和slice這么相似,底層指向的也是數(shù)組,是什么數(shù)組呢?我們看看他在實(shí)例化時(shí)調(diào)用的方法:

    //go:nosplit
    func gostringnocopy(str *byte) string {
    	ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
    	s := *(*string)(unsafe.Pointer(&ss))
    	return s
    }

    入?yún)⑹且粋€(gè)byte類(lèi)型的指針,從這我們可以看出string類(lèi)型底層是一個(gè)byte類(lèi)型的數(shù)組,所以我們可以畫(huà)出這樣一個(gè)圖片:

    Go語(yǔ)言怎么高效的進(jìn)行字符串拼接

    string類(lèi)型本質(zhì)上就是一個(gè)byte類(lèi)型的數(shù)組,在Go語(yǔ)言中string類(lèi)型被設(shè)計(jì)為不可變的,不僅是在Go語(yǔ)言,其他語(yǔ)言中string類(lèi)型也是被設(shè)計(jì)為不可變的,這樣的好處就是:在并發(fā)場(chǎng)景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔(dān)心安全問(wèn)題。

    string類(lèi)型雖然是不能更改的,但是可以被替換,因?yàn)?code>stringStruct中的str指針是可以改變的,只是指針指向的內(nèi)容是不可以改變的,也就說(shuō)每一個(gè)更改字符串,就需要重新分配一次內(nèi)存,之前分配的空間會(huì)被gc回收。

    字符串拼接的6種方式及原理

    原生拼接方式"+"

    Go語(yǔ)言原生支持使用+操作符直接對(duì)兩個(gè)字符串進(jìn)行拼接,使用例子如下:

    var s string
    s += "asong"
    s += "真帥"

    這種方式使用起來(lái)最簡(jiǎn)單,基本所有語(yǔ)言都有提供這種方式,使用+操作符進(jìn)行拼接時(shí),會(huì)對(duì)字符串進(jìn)行遍歷,計(jì)算并開(kāi)辟一個(gè)新的空間來(lái)存儲(chǔ)原來(lái)的兩個(gè)字符串。

    字符串格式化函數(shù)fmt.Sprintf

    Go語(yǔ)言中默認(rèn)使用函數(shù)fmt.Sprintf進(jìn)行字符串格式化,所以也可使用這種方式進(jìn)行字符串拼接:

    str := "asong"
    str = fmt.Sprintf("%s%s", str, str)

    fmt.Sprintf實(shí)現(xiàn)原理主要是使用到了反射,具體源碼分析因?yàn)槠脑蚓筒辉谶@里詳細(xì)分析了,看到反射,就會(huì)產(chǎn)生性能的損耗,你們懂得?。?!

    Strings.builder

    Go語(yǔ)言提供了一個(gè)專(zhuān)門(mén)操作字符串的庫(kù)strings,使用strings.Builder可以進(jìn)行字符串拼接,提供了writeString方法拼接字符串,使用方式如下:

    var builder strings.Builder
    builder.WriteString("asong")
    builder.String()

    strings.builder的實(shí)現(xiàn)原理很簡(jiǎn)單,結(jié)構(gòu)如下:

    type Builder struct {
        addr *Builder // of receiver, to detect copies by value
        buf  []byte // 1
    }

    addr字段主要是做copycheck,buf字段是一個(gè)byte類(lèi)型的切片,這個(gè)就是用來(lái)存放字符串內(nèi)容的,提供的writeString()方法就是像切片buf中追加數(shù)據(jù):

    func (b *Builder) WriteString(s string) (int, error) {
    	b.copyCheck()
    	b.buf = append(b.buf, s...)
    	return len(s), nil
    }

    提供的String方法就是將[]]byte轉(zhuǎn)換為string類(lèi)型,這里為了避免內(nèi)存拷貝的問(wèn)題,使用了強(qiáng)制轉(zhuǎn)換來(lái)避免內(nèi)存拷貝:

    func (b *Builder) String() string {
    	return *(*string)(unsafe.Pointer(&b.buf))
    }

    bytes.Buffer

    因?yàn)?code>string類(lèi)型底層就是一個(gè)byte數(shù)組,所以我們就可以Go語(yǔ)言的bytes.Buffer進(jìn)行字符串拼接。bytes.Buffer是一個(gè)一個(gè)緩沖byte類(lèi)型的緩沖器,這個(gè)緩沖器里存放著都是byte。使用方式如下:

    buf := new(bytes.Buffer)
    buf.WriteString("asong")
    buf.String()

    bytes.buffer底層也是一個(gè)[]byte切片,結(jié)構(gòu)體如下:

    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.
    }

    因?yàn)?code>bytes.Buffer可以持續(xù)向Buffer尾部寫(xiě)入數(shù)據(jù),從Buffer頭部讀取數(shù)據(jù),所以off字段用來(lái)記錄讀取位置,再利用切片的cap特性來(lái)知道寫(xiě)入位置,這個(gè)不是本次的重點(diǎn),重點(diǎn)看一下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
    }

    切片在創(chuàng)建時(shí)并不會(huì)申請(qǐng)內(nèi)存塊,只有在往里寫(xiě)數(shù)據(jù)時(shí)才會(huì)申請(qǐng),首次申請(qǐng)的大小即為寫(xiě)入數(shù)據(jù)的大小。如果寫(xiě)入的數(shù)據(jù)小于64字節(jié),則按64字節(jié)申請(qǐng)。采用動(dòng)態(tài)擴(kuò)展slice的機(jī)制,字符串追加采用copy的方式將追加的部分拷貝到尾部,copy是內(nèi)置的拷貝函數(shù),可以減少內(nèi)存分配。

    但是在將[]byte轉(zhuǎn)換為string類(lèi)型依舊使用了標(biāo)準(zhǔn)類(lèi)型,所以會(huì)發(fā)生內(nèi)存分配:

    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方法可以將一個(gè)string類(lèi)型的切片拼接成一個(gè)字符串,可以定義連接操作符,使用如下:

    baseSlice := []string{"asong", "真帥"}
    strings.Join(baseSlice, "")

    strings.join也是基于strings.builder來(lái)實(shí)現(xiàn)的,代碼如下:

    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方法內(nèi)調(diào)用了b.Grow(n)方法,這個(gè)是進(jìn)行初步的容量分配,而前面計(jì)算的n的長(zhǎng)度就是我們要拼接的slice的長(zhǎng)度,因?yàn)槲覀儌魅肭衅L(zhǎng)度固定,所以提前進(jìn)行容量分配可以減少內(nèi)存分配,很高效。

    切片append

    因?yàn)?code>string類(lèi)型底層也是byte類(lèi)型數(shù)組,所以我們可以重新聲明一個(gè)切片,使用append進(jìn)行字符串拼接,使用方式如下:

    buf := make([]byte, 0)
    base = "asong"
    buf = append(buf, base...)
    string(base)

    如果想減少內(nèi)存分配,在將[]byte轉(zhuǎn)換為string類(lèi)型時(shí)可以考慮使用強(qiáng)制轉(zhuǎn)換。

    Benchmark對(duì)比

    上面我們總共提供了6種方法,原理我們基本知道了,那么我們就使用Go語(yǔ)言中的Benchmark來(lái)分析一下到底哪種字符串拼接方式更高效。我們主要分兩種情況進(jìn)行分析:

    • 少量字符串拼接

    • 大量字符串拼接

    我們先定義一個(gè)基礎(chǔ)字符串:

    var base  = "123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"

    少量字符串拼接的測(cè)試我們就采用拼接一次的方式驗(yàn)證,base拼接base,因此得出benckmark結(jié)果:

    goos: darwin
    goarch: amd64
    pkg: asong.cloud/Golang_Dream/code_demo/string_join/once
    cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    BenchmarkSumString-16           21338802                49.19 ns/op          128 B/op          1 allocs/op
    BenchmarkSprintfString-16        7887808               140.5 ns/op           160 B/op          3 allocs/op
    BenchmarkBuilderString-16       27084855                41.39 ns/op          128 B/op          1 allocs/op
    BenchmarkBytesBuffString-16      9546277               126.0 ns/op           384 B/op          3 allocs/op
    BenchmarkJoinstring-16          24617538                48.21 ns/op          128 B/op          1 allocs/op
    BenchmarkByteSliceString-16     10347416               112.7 ns/op           320 B/op          3 allocs/op
    PASS
    ok      asong.cloud/Golang_Dream/code_demo/string_join/once     8.412s

    大量字符串拼接的測(cè)試我們先構(gòu)建一個(gè)長(zhǎng)度為200的字符串切片:

    var baseSlice []string
    for i := 0; i < 200; i++ {
    		baseSlice = append(baseSlice, base)
    }

    然后遍歷這個(gè)切片不斷的進(jìn)行拼接,因?yàn)榭梢缘贸?code>benchmark:

    goos: darwin
    goarch: amd64
    pkg: asong.cloud/Golang_Dream/code_demo/string_join/muliti
    cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    BenchmarkSumString-16                       7396            163612 ns/op         1277713 B/op        199 allocs/op
    BenchmarkSprintfString-16                   5946            202230 ns/op         1288552 B/op        600 allocs/op
    BenchmarkBuilderString-16                 262525              4638 ns/op           40960 B/op          1 allocs/op
    BenchmarkBytesBufferString-16             183492              6568 ns/op           44736 B/op          9 allocs/op
    BenchmarkJoinstring-16                    398923              3035 ns/op           12288 B/op          1 allocs/op
    BenchmarkByteSliceString-16               144554              8205 ns/op           60736 B/op         15 allocs/op
    PASS
    ok      asong.cloud/Golang_Dream/code_demo/string_join/muliti   10.699s

    結(jié)論

    通過(guò)兩次benchmark對(duì)比,我們可以看到當(dāng)進(jìn)行少量字符串拼接時(shí),直接使用+操作符進(jìn)行拼接字符串,效率還是挺高的,但是當(dāng)要拼接的字符串?dāng)?shù)量上來(lái)時(shí),+操作符的性能就比較低了;函數(shù)fmt.Sprintf還是不適合進(jìn)行字符串拼接,無(wú)論拼接字符串?dāng)?shù)量多少,性能損耗都很大,還是老老實(shí)實(shí)做他的字符串格式化就好了;strings.Builder無(wú)論是少量字符串的拼接還是大量的字符串拼接,性能一直都能穩(wěn)定,這也是為什么Go語(yǔ)言官方推薦使用strings.builder進(jìn)行字符串拼接的原因,在使用strings.builder時(shí)最好使用Grow方法進(jìn)行初步的容量分配,觀(guān)察strings.join方法的benchmark就可以發(fā)現(xiàn),因?yàn)槭褂昧?code>grow方法,提前分配好內(nèi)存,在字符串拼接的過(guò)程中,不需要進(jìn)行字符串的拷貝,也不需要分配新的內(nèi)存,這樣使用strings.builder性能最好,且內(nèi)存消耗最小。bytes.Buffer方法性能是低于strings.builder的,bytes.Buffer 轉(zhuǎn)化為字符串時(shí)重新申請(qǐng)了一塊空間,存放生成的字符串變量,不像strings.buidler這樣直接將底層的 []byte 轉(zhuǎn)換成了字符串類(lèi)型返回,這就占用了更多的空間。

    同步最后分析的結(jié)論:

    無(wú)論什么情況下使用strings.builder進(jìn)行字符串拼接都是最高效的,不過(guò)要主要使用方法,記得調(diào)用grow進(jìn)行容量分配,才會(huì)高效。strings.join的性能約等于strings.builder,在已經(jīng)字符串slice的時(shí)候可以使用,未知時(shí)不建議使用,構(gòu)造切片也是有性能損耗的;如果進(jìn)行少量的字符串拼接時(shí),直接使用+操作符是最方便也是性能最高的,可以放棄strings.builder的使用。

    綜合對(duì)比性能排序:

    strings.join &asymp; strings.builder > bytes.buffer > []byte轉(zhuǎn)換string > "+" > fmt.sprintf

    到此,相信大家對(duì)“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI