<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 語言如何高效的進(jìn)行字符串拼接(6 種方式進(jìn)行對(duì)比分析)

          共 3849字,需瀏覽 8分鐘

           ·

          2022-01-12 12:54

          前言

          日常業(yè)務(wù)開發(fā)中離不開字符串的拼接操作,不同語言的字符串實(shí)現(xiàn)方式都不同,在Go語言中就提供了6種方式進(jìn)行字符串拼接,那這幾種拼接方式該如何選擇呢?使用那個(gè)更高效呢?本文我們就一起來分析一下。

          本文使用Go語言版本:1.17.1

          string類型

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

          //?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類型本質(zhì)也是一個(gè)結(jié)構(gòu)體,定義如下:

          type?stringStruct?struct?{
          ????str?unsafe.Pointer
          ????len?int
          }

          stringStructslice還是很相似的,str指針指向的是某個(gè)數(shù)組的首地址,len代表的就是數(shù)組長度。怎么和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類型的指針,從這我們可以看出string類型底層是一個(gè)byte類型的數(shù)組,所以我們可以畫出這樣一個(gè)圖片:

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

          string類型雖然是不能更改的,但是可以被替換,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">stringStruct中的str指針是可以改變的,只是指針指向的內(nèi)容是不可以改變的,也就說每一個(gè)更改字符串,就需要重新分配一次內(nèi)存,之前分配的空間會(huì)被gc回收。

          關(guān)于string類型的知識(shí)點(diǎn)就描述這么多,方便我們后面分析字符串拼接。

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

          原生拼接方式"+"

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

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

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

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

          Go語言中默認(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語言提供了一個(gè)專門操作字符串的庫strings,使用strings.Builder可以進(jìn)行字符串拼接,提供了writeString方法拼接字符串,使用方式如下:

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

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

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

          addr字段主要是做copycheckbuf字段是一個(gè)byte類型的切片,這個(gè)就是用來存放字符串內(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類型,這里為了避免內(nèi)存拷貝的問題,使用了強(qiáng)制轉(zhuǎn)換來避免內(nèi)存拷貝:

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

          bytes.Buffer

          因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">string類型底層就是一個(gè)byte數(shù)組,所以我們就可以Go語言的bytes.Buffer進(jìn)行字符串拼接。bytes.Buffer是一個(gè)一個(gè)緩沖byte類型的緩沖器,這個(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 style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">bytes.Buffer可以持續(xù)向Buffer尾部寫入數(shù)據(jù),從Buffer頭部讀取數(shù)據(jù),所以off字段用來記錄讀取位置,再利用切片的cap特性來知道寫入位置,這個(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)存塊,只有在往里寫數(shù)據(jù)時(shí)才會(huì)申請(qǐng),首次申請(qǐng)的大小即為寫入數(shù)據(jù)的大小。如果寫入的數(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類型依舊使用了標(biāo)準(zhǔn)類型,所以會(huì)發(fā)生內(nèi)存分配:

          func?(b?*Buffer)?String()?string?{
          ?if?b?==?nil?{
          ??//?Special?case,?useful?in?debugging.
          ??return?""
          ?}
          ?return?string(b.buf[b.off:])
          }

          strings.join

          Strings.join方法可以將一個(gè)string類型的切片拼接成一個(gè)字符串,可以定義連接操作符,使用如下:

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

          strings.join也是基于strings.builder來實(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的長度就是我們要拼接的slice的長度,因?yàn)槲覀儌魅肭衅L度固定,所以提前進(jìn)行容量分配可以減少內(nèi)存分配,很高效。

          切片append

          因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">string類型底層也是byte類型數(shù)組,所以我們可以重新聲明一個(gè)切片,使用append進(jìn)行字符串拼接,使用方式如下:

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

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

          Benchmark對(duì)比

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

          • 少量字符串拼接
          • 大量字符串拼接

          因?yàn)榇a量有點(diǎn)多,下面只貼出分析結(jié)果,詳細(xì)代碼已經(jīng)上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join

          我們先定義一個(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è)長度為200的字符串切片:

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

          然后遍歷這個(gè)切片不斷的進(jìn)行拼接,因?yàn)榭梢缘贸?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">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é)論

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

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

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

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

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

          總結(jié)

          本文我們針對(duì)6種字符串的拼接方式進(jìn)行介紹,并通過benckmark對(duì)比了效率,無論什么時(shí)候使用strings.builder都不會(huì)錯(cuò),但是在少量字符串拼接時(shí),直接+也就是更優(yōu)的方式,具體業(yè)務(wù)場(chǎng)景具體分析,不要一概而論。

          文中代碼已上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join


          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人免费大片黄在线播放 | 国产黄色片网站 | 色天堂在线观看视频 | 在一起做爱视频 | av天堂首页 |