go字符串拼接方式及性能比拼小結(jié)
在golang中字符串的拼接方式有多種,本文將會介紹比較常用的幾種方式,并且對各種方式進(jìn)行壓測,以此來得到在不同場景下更適合使用的方案。
1、go字符串的幾種拼接方式
比如對于三個字符串,s1、s2、s3,需要將其拼接為一個字符串,有如下的幾種方式:
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、性能測試
上面介紹了5種字符串的拼接方式,那么它們的性能如何呢,接下來將對這五種字符串拼接進(jìn)行一個性能測試:
go版本:go1.21.0
如下為性能測試的結(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ò)容)
性能測試的結(jié)果如下(僅供參考):
拼接的字符串?dāng)?shù)量:3, 字符串長度:10, 性能如下

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

當(dāng)字符串?dāng)?shù)量較多和長度較大時,性能從高到低:
strings.Builder(先Grow) > +拼接 > strings.Join > bytes.Buffer(先Grow) > fmt.Sprintf > strings.Builder > +拼接(使用for循環(huán)) > bytes.Buffer
從上面的壓測來看,直接使用+拼接字符串和使用strings.Builder(需要先grow)以及使用strings.Join的性能都是不錯的。上面有幾個重點(diǎn)需要關(guān)注的點(diǎn):
1. 當(dāng)字符串?dāng)?shù)量較少長度較小時,使用+來拼接字符串的效率非常高并且內(nèi)存分配次數(shù)為0(棧內(nèi)存分配)
2. 當(dāng)字符串?dāng)?shù)量較少長度較小時,bytes.Grow使用和不使用區(qū)別不大 (bytes.Buffer的最小擴(kuò)容容量為64)
3. fmt.Sprintf的內(nèi)存分配次數(shù)最多(涉及大量的interface{}操作,導(dǎo)致逃逸)
接下來將從源碼的角度來分析它們的性能
3、源碼分析
注意:go的版本為1.21.0
3.1 +拼接
如果從感覺上來講,我們通常會認(rèn)為使用+來拼接字符串肯定是最低效的,因為會有多次字符串的拷貝,結(jié)果不然,接下來從源碼的角度進(jìn)行分析,看為什么使用+來拼接字符串的效率是非常高的:
源碼位于runtime/string.go下:
concatstrings實現(xiàn)了go的字符串+拼接,所有的字符串會被放入一個字符串切片中,并且會傳入一個大小為32字節(jié)的字符數(shù)組。
如果拼接后的字符串長度較小并且不會發(fā)生逃逸,那么就會在棧上創(chuàng)建出大小為32字節(jié)的字符數(shù)組。
步驟如下:
- 首先計算拼接后的字符串的長度;
- 如果編譯器可以確定拼接后的字符串不會發(fā)生逃逸,buf就不為nil,如果buf不為nil并且buf可以存放下拼接后的字符串,就使用buf
- 如果buf為nil或者大小不足,則會在堆上申請出一片可以存放下拼接后的字符串的空間,然后將字符串一個一個拷貝過去
// 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 {
// 首先計算出拼接后的字符串的長度
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 ""
}
// 如果只有一個字符串并且它不在棧上或者我們的結(jié)果沒有轉(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)
}
通過上面的源碼分析,可以得知,使用直接使用+拼接字符串會先申請出一片內(nèi)存,然后將字符串一個一個拷貝過去,并且字符串有可能分配在棧上,因此效率非常高。
但是在使用for循環(huán)來拼接時,由于編譯器無法確定最終的內(nèi)存空間大小,因此會發(fā)生多次拷貝,效率很低。
當(dāng)字符串比較小并且數(shù)量是已知的時,使用+拼接字符串的效率很高,并且代碼可讀性更好。
3.2 strings.Builder
除了使用+來拼接字符串,通常string.Builder使用的也是非常多的,并且它的效率相比也是更高的,接下來看一下Builder的實現(xiàn)
在Builder中有一個字節(jié)切片的buf,每次在寫入時都會追加到buf中,當(dāng)buf容量不足時,切片會自動擴(kuò)容,但是在擴(kuò)容時會拷貝舊的切片,因此如果預(yù)先使用Grow來分配內(nèi)存,則可以減少擴(kuò)容時的拷貝開銷,從而提高效率。
另一個高效的原因是在使用String()獲取字符串時直接共用了切片的底層存儲數(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在獲取字符串時返回的string和buf共用同一片字符數(shù)組,因此減少了一次數(shù)據(jù)拷貝。在使用時,使用grow預(yù)先分配內(nèi)存可以減少切片擴(kuò)容時的數(shù)據(jù)拷貝,提高性能,因此建議先使用Grow進(jìn)行預(yù)分配
3.3 strings.Join
在上面的性能測試中,Join的性能也很高,因為strings.join本身使用了strings.Builder,并且在拼接字符串之前使用Grow進(jìn)行了內(nèi)存預(yù)分配,因此效率也很高。
代碼很簡單,就不再介紹。
3.4 bytes.Buffer
bytes.Buffer和strings.Builder比較相似,但是通常用于處理字節(jié)數(shù)據(jù),而不是字符串。一個區(qū)別就是在使用String()方法來獲取字符串時,有一次切片到字符串的拷貝,因此效率不如strings.Buffer但是當(dāng)字符串長度較小時,bytes.Buffer的效率甚至比strings.Buffer要高。是因為,Builder的擴(kuò)容是按照切片的擴(kuò)容策略來的,而Buffer的初始最小擴(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的實現(xiàn)較為復(fù)雜,并且使用了大量的interface{},會導(dǎo)致內(nèi)存逃逸,涉及到多次內(nèi)存分配,效率較低。如果是純字符串,通常不會使用fmt.Sprintf來進(jìn)行拼接,fmt.Sprintf可以對多種數(shù)據(jù)格式進(jìn)行字符串格式化。
總結(jié):
1.當(dāng)要拼接的多個字符串是已知并且數(shù)量較少時,可以直接使用+來拼接,效率比較高而且可讀性更好
2、當(dāng)要拼接的字符串?dāng)?shù)量和長度未知時,可以使用strings.Builder來拼接,并且預(yù)估字符串的大小使用Grow進(jìn)行預(yù)分配,效率較高
3、當(dāng)要拼接的字符串?dāng)?shù)量已知或者在拼接時需要加入分割字符串時,可以使用strings.Join,效率較高,也很方便
4、在進(jìn)行字節(jié)數(shù)據(jù)處理時可以使用bytes.Buffer
5、當(dāng)要對包含多種格式的數(shù)據(jù)進(jìn)行字符串格式化時使用fmt.Sprintf,更加方便
壓測代碼:
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go?singleflight緩存雪崩源碼分析與應(yīng)用
這篇文章主要為大家介紹了go?singleflight緩存雪崩源碼分析與應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Golang中優(yōu)秀的消息隊列NSQ基礎(chǔ)安裝及使用詳解
這篇文章主要介紹了Golang中優(yōu)秀的消息隊列NSQ基礎(chǔ)安裝及使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類型
Go?語言的?sync?包提供了一系列同步原語,其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下2023-05-05

