go字符串拼接方式及性能比拼小結(jié)
在golang中字符串的拼接方式有多種,本文將會(huì)介紹比較常用的幾種方式,并且對(duì)各種方式進(jìn)行壓測(cè),以此來(lái)得到在不同場(chǎng)景下更適合使用的方案。
1、go字符串的幾種拼接方式
比如對(duì)于三個(gè)字符串,s1、s2、s3,需要將其拼接為一個(gè)字符串,有如下的幾種方式:
1.1 fmt.Sprintf
s := fmt.Sprintf("%s%s%s", s1, s2, s3)
1.2 +運(yùn)算符拼接
s := s1 + s2 + s3
1.3 strings.Join
s := strings.Join([]string{s1, s2, s3}, "")
1.4 strings.Builder
builder := strings.Builder{}
builder.WriteString(s1)
builder.WriteString(s2)
builder.WriteString(s3)
s := builder.String()
1.5 bytes.Buffer
buffer := bytes.Buffer{}
buffer.WriteString(s1)
buffer.WriteString(s2)
buffer.WriteString(s3)
s := buffer.String()
2、性能測(cè)試
上面介紹了5種字符串的拼接方式,那么它們的性能如何呢,接下來(lái)將對(duì)這五種字符串拼接進(jìn)行一個(gè)性能測(cè)試:
go版本:go1.21.0
如下為性能測(cè)試的結(jié)果,代碼將在最后面給出,總共有八種,分別為:
1.fmt.Sprintf
2.+
3.使用for循環(huán)和+拼接
4.strings.join
5.strings.Builder
6.strings.Builder(先使用Grow擴(kuò)容)
7.bytes.Buffer
8.bytes.Buffer(先使用Grow擴(kuò)容)
性能測(cè)試的結(jié)果如下(僅供參考):
拼接的字符串?dāng)?shù)量:3, 字符串長(zhǎng)度:10, 性能如下

當(dāng)字符串?dāng)?shù)量和長(zhǎng)度較小時(shí),性能從高到低:
+拼接 > strings.Builder(先Grow) > strings.Join > bytes.Buffer > bytes.Buffer(先Grow) > strings.Builder > +拼接(使用for循環(huán)) > fmt.Sprintf
拼接的字符串?dāng)?shù)量:5, 字符串長(zhǎng)度:128, 性能如下

當(dāng)字符串?dāng)?shù)量較多和長(zhǎng)度較大時(shí),性能從高到低:
strings.Builder(先Grow) > +拼接 > strings.Join > bytes.Buffer(先Grow) > fmt.Sprintf > strings.Builder > +拼接(使用for循環(huán)) > bytes.Buffer
從上面的壓測(cè)來(lái)看,直接使用+拼接字符串和使用strings.Builder(需要先grow)以及使用strings.Join的性能都是不錯(cuò)的。上面有幾個(gè)重點(diǎn)需要關(guān)注的點(diǎn):
1. 當(dāng)字符串?dāng)?shù)量較少長(zhǎng)度較小時(shí),使用+來(lái)拼接字符串的效率非常高并且內(nèi)存分配次數(shù)為0(棧內(nèi)存分配)
2. 當(dāng)字符串?dāng)?shù)量較少長(zhǎng)度較小時(shí),bytes.Grow使用和不使用區(qū)別不大 (bytes.Buffer的最小擴(kuò)容容量為64)
3. fmt.Sprintf的內(nèi)存分配次數(shù)最多(涉及大量的interface{}操作,導(dǎo)致逃逸)
接下來(lái)將從源碼的角度來(lái)分析它們的性能
3、源碼分析
注意:go的版本為1.21.0
3.1 +拼接
如果從感覺(jué)上來(lái)講,我們通常會(huì)認(rèn)為使用+來(lái)拼接字符串肯定是最低效的,因?yàn)闀?huì)有多次字符串的拷貝,結(jié)果不然,接下來(lái)從源碼的角度進(jìn)行分析,看為什么使用+來(lái)拼接字符串的效率是非常高的:
源碼位于runtime/string.go下:
concatstrings實(shí)現(xiàn)了go的字符串+拼接,所有的字符串會(huì)被放入一個(gè)字符串切片中,并且會(huì)傳入一個(gè)大小為32字節(jié)的字符數(shù)組。
如果拼接后的字符串長(zhǎng)度較小并且不會(huì)發(fā)生逃逸,那么就會(huì)在棧上創(chuàng)建出大小為32字節(jié)的字符數(shù)組。
步驟如下:
- 首先計(jì)算拼接后的字符串的長(zhǎng)度;
- 如果編譯器可以確定拼接后的字符串不會(huì)發(fā)生逃逸,buf就不為nil,如果buf不為nil并且buf可以存放下拼接后的字符串,就使用buf
- 如果buf為nil或者大小不足,則會(huì)在堆上申請(qǐng)出一片可以存放下拼接后的字符串的空間,然后將字符串一個(gè)一個(gè)拷貝過(guò)去
// The constant is known to the compiler.
// There is no fundamental theory behind this number.
const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
// concatstrings implements a Go string concatenation x+y+z+...
func concatstrings(buf *tmpBuf, a []string) string {
// 首先計(jì)算出拼接后的字符串的長(zhǎng)度
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
if n == 0 {
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}
// 如果只有一個(gè)字符串并且它不在棧上或者我們的結(jié)果沒(méi)有轉(zhuǎn)義調(diào)用幀(但是f != nil),那么我們可以直接返回該字符串。
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
s, b := rawstringtmp(buf, l)
for _, x := range a {
copy(b, x)
b = b[len(x):]
}
return s
}
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
// 如果buf不為nil而且buf可以存放下拼接后的字符串,就直接使用buf
if buf != nil && l <= len(buf) {
b = buf[:l]
s = slicebytetostringtmp(&b[0], len(b))
} else {
// 否則在堆上分配一片區(qū)域
s, b = rawstring(l)
}
return
}
// 在堆上分配一片內(nèi)存,并且返回底層字符串結(jié)構(gòu)和切片結(jié)構(gòu),它們指向同一片內(nèi)存
func rawstring(size int) (s string, b []byte) {
p := mallocgc(uintptr(size), nil, false)
return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}
通過(guò)上面的源碼分析,可以得知,使用直接使用+拼接字符串會(huì)先申請(qǐng)出一片內(nèi)存,然后將字符串一個(gè)一個(gè)拷貝過(guò)去,并且字符串有可能分配在棧上,因此效率非常高。
但是在使用for循環(huán)來(lái)拼接時(shí),由于編譯器無(wú)法確定最終的內(nèi)存空間大小,因此會(huì)發(fā)生多次拷貝,效率很低。
當(dāng)字符串比較小并且數(shù)量是已知的時(shí),使用+拼接字符串的效率很高,并且代碼可讀性更好。
3.2 strings.Builder
除了使用+來(lái)拼接字符串,通常string.Builder使用的也是非常多的,并且它的效率相比也是更高的,接下來(lái)看一下Builder的實(shí)現(xiàn)
在Builder中有一個(gè)字節(jié)切片的buf,每次在寫(xiě)入時(shí)都會(huì)追加到buf中,當(dāng)buf容量不足時(shí),切片會(huì)自動(dòng)擴(kuò)容,但是在擴(kuò)容時(shí)會(huì)拷貝舊的切片,因此如果預(yù)先使用Grow來(lái)分配內(nèi)存,則可以減少擴(kuò)容時(shí)的拷貝開(kāi)銷(xiāo),從而提高效率。
另一個(gè)高效的原因是在使用String()獲取字符串時(shí)直接共用了切片的底層存儲(chǔ)數(shù)組,從而減少了一次數(shù)據(jù)的拷貝。因此Builder的所有api都是只能追加,不能修改的。
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
func (b *Builder) grow(n int) {
buf := bytealg.MakeNoZero(2*cap(b.buf) + n)[:len(b.buf)]
copy(buf, b.buf)
b.buf = buf
}
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
// 返回的string和buf共用了同一片底層字符數(shù)組,減少了數(shù)據(jù)拷貝
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
strings.Builder在獲取字符串時(shí)返回的string和buf共用同一片字符數(shù)組,因此減少了一次數(shù)據(jù)拷貝。在使用時(shí),使用grow預(yù)先分配內(nèi)存可以減少切片擴(kuò)容時(shí)的數(shù)據(jù)拷貝,提高性能,因此建議先使用Grow進(jìn)行預(yù)分配
3.3 strings.Join
在上面的性能測(cè)試中,Join的性能也很高,因?yàn)閟trings.join本身使用了strings.Builder,并且在拼接字符串之前使用Grow進(jìn)行了內(nèi)存預(yù)分配,因此效率也很高。
代碼很簡(jiǎn)單,就不再介紹。
3.4 bytes.Buffer
bytes.Buffer和strings.Builder比較相似,但是通常用于處理字節(jié)數(shù)據(jù),而不是字符串。一個(gè)區(qū)別就是在使用String()方法來(lái)獲取字符串時(shí),有一次切片到字符串的拷貝,因此效率不如strings.Buffer但是當(dāng)字符串長(zhǎng)度較小時(shí),bytes.Buffer的效率甚至比strings.Buffer要高。是因?yàn)?,Builder的擴(kuò)容是按照切片的擴(kuò)容策略來(lái)的,而B(niǎo)uffer的初始最小擴(kuò)容大小為64,也就是第一次擴(kuò)容最小大小為64,因此使用Grow和不使用的區(qū)別不大。
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
const smallBufferSize = 64
func (b *Buffer) grow(n int) int {
...
if b.buf == nil && n <= smallBufferSize {
b.buf = make([]byte, n, smallBufferSize)
return 0
}
...
}
3.5 fmt.Sprintf
fmt.Sprintf的實(shí)現(xiàn)較為復(fù)雜,并且使用了大量的interface{},會(huì)導(dǎo)致內(nèi)存逃逸,涉及到多次內(nèi)存分配,效率較低。如果是純字符串,通常不會(huì)使用fmt.Sprintf來(lái)進(jìn)行拼接,fmt.Sprintf可以對(duì)多種數(shù)據(jù)格式進(jìn)行字符串格式化。
總結(jié):
1.當(dāng)要拼接的多個(gè)字符串是已知并且數(shù)量較少時(shí),可以直接使用+來(lái)拼接,效率比較高而且可讀性更好
2、當(dāng)要拼接的字符串?dāng)?shù)量和長(zhǎng)度未知時(shí),可以使用strings.Builder來(lái)拼接,并且預(yù)估字符串的大小使用Grow進(jìn)行預(yù)分配,效率較高
3、當(dāng)要拼接的字符串?dāng)?shù)量已知或者在拼接時(shí)需要加入分割字符串時(shí),可以使用strings.Join,效率較高,也很方便
4、在進(jìn)行字節(jié)數(shù)據(jù)處理時(shí)可以使用bytes.Buffer
5、當(dāng)要對(duì)包含多種格式的數(shù)據(jù)進(jìn)行字符串格式化時(shí)使用fmt.Sprintf,更加方便
壓測(cè)代碼:
package string_concats
import (
"bytes"
"fmt"
"math/rand"
"strings"
"testing"
"time"
)
const dic = "qwertyuioplkjhgfdsazxcvbnmMNBVCXZASDFGHJKLPOIUYTREWQ0123456789"
var defaultRand = rand.New(rand.NewSource(time.Now().UnixNano()))
func RandString(n int) string {
builder := strings.Builder{}
builder.Grow(n)
for i := 0; i < n; i++ {
n := defaultRand.Intn(len(dic))
builder.WriteByte(dic[n])
}
return builder.String()
}
var (
strs []string
N = 5
Len = 128
)
func init() {
for i := 0; i < N; i++ {
strs = append(strs, RandString(Len))
}
}
// fmt.Sprintf
func BenchmarkSprintf(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s%s%s%s%s", strs[0], strs[1], strs[2], strs[3], strs[4])
}
}
// s1 + s2 + s3
func BenchmarkConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strs[0] + strs[1] + strs[2] + strs[3] + strs[4]
}
}
// for循環(huán)+
func BenchmarkForConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for i := 0; i < len(strs); i++ {
s += strs[i]
}
}
}
// strings.Join
func BenchmarkJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(strs, "")
}
}
// strings.Builder
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
builder := strings.Builder{}
for i := 0; i < len(strs); i++ {
builder.WriteString(strs[i])
}
_ = builder.String()
}
}
// strings.Builder
func BenchmarkBuilderGrowFirst(b *testing.B) {
for i := 0; i < b.N; i++ {
builder := strings.Builder{}
n := 0
for i := 0; i < len(strs); i++ {
n += len(strs[i])
}
builder.Grow(n)
for i := 0; i < len(strs); i++ {
builder.WriteString(strs[i])
}
_ = builder.String()
}
}
// bytes.Buffer
func BenchmarkBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
buffer := bytes.Buffer{}
for i := 0; i < len(strs); i++ {
buffer.WriteString(strs[i])
}
_ = buffer.String()
}
}
// bytes.Buffer
func BenchmarkBufferGrowFirst(b *testing.B) {
for i := 0; i < b.N; i++ {
buffer := bytes.Buffer{}
n := 0
for i := 0; i < len(strs); i++ {
n += len(strs[i])
}
buffer.Grow(n)
for i := 0; i < len(strs); i++ {
buffer.WriteString(strs[i])
}
_ = buffer.String()
}
}
到此這篇關(guān)于go字符串拼接方式及性能比拼小結(jié)的文章就介紹到這了,更多相關(guān)go字符串拼接內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go?singleflight緩存雪崩源碼分析與應(yīng)用
這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解
這篇文章主要介紹了Golang中優(yōu)秀的消息隊(duì)列NSQ基礎(chǔ)安裝及使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
golang如何實(shí)現(xiàn)三元運(yùn)算符功能
這篇文章主要介紹了在其他一些編程語(yǔ)言中,如?C?語(yǔ)言,三元運(yùn)算符是一種可以用一行代碼實(shí)現(xiàn)條件選擇的簡(jiǎn)便方法,那么在Go語(yǔ)言中如何實(shí)現(xiàn)類(lèi)似功能呢,下面就跟隨小編一起學(xué)習(xí)一下吧2024-02-02
深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類(lèi)型
Go?語(yǔ)言的?sync?包提供了一系列同步原語(yǔ),其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實(shí)現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下2023-05-05
淺析Golang中類(lèi)型嵌入的簡(jiǎn)介與使用
類(lèi)型嵌入指的就是在一個(gè)類(lèi)型的定義中嵌入了其他類(lèi)型,Go?語(yǔ)言支持兩種類(lèi)型嵌入,分別是接口類(lèi)型的類(lèi)型嵌入和結(jié)構(gòu)體類(lèi)型的類(lèi)型嵌入,下面我們就來(lái)詳細(xì)一下類(lèi)型嵌入的使用吧2023-11-11
golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了golang 生成二維碼海報(bào)的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
詳解Golang中g(shù)omock的使用場(chǎng)景和方法
gomock是Go編程語(yǔ)言的模擬框架, 它與Go的內(nèi)置測(cè)試包很好地集成在一起,但也可以在其他上下文中使用,本文主要介紹了gomock的使用場(chǎng)景和方法,感興趣的可以了解下2024-10-10

