先上代碼
// fmt.Sprintf
func BenchmarkStringSprintf(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var str string
for j := 0; j < numbers; j++ {
str = fmt.Sprintf("%s%d", str, j)
}
}
b.StopTimer()
}
// add
func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var str string
for j := 0; j < numbers; j++ {
str = str + string(j)
}
}
b.StopTimer()
}
// bytes.Buffer
func BenchmarkStringBuffer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
for j := 0; j < numbers; j++ {
buffer.WriteString(strconv.Itoa(j))
}
_ = buffer.String()
}
b.StopTimer()
}
// strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < numbers; j++ {
builder.WriteString(strconv.Itoa(j))
}
_ = builder.String()
}
b.StopTimer()
}
運(yùn)行結(jié)果
lijun:benchmark/ $ go test -bench=. [10:01:20]
goos: darwin
goarch: amd64
pkg: class12/benchmark
BenchmarkStringSprintf-4 30 47358694 ns/op
BenchmarkStringAdd-4 50 27664814 ns/op
BenchmarkStringBuffer-4 10000 184422 ns/op
BenchmarkStringBuilder-4 10000 157039 ns/op
PASS
ok class12/benchmark 6.350s
lijun:benchmark/ $ [10:01:58]
如果還不知道 Go 的 benchmark腔长,可以先去了解一下草则,個(gè)人認(rèn)為還是非常不錯(cuò)的性能測(cè)試的工具摧莽。
得出結(jié)論
四種拼接字符串的方式,性能比較結(jié)果
strings.Builder > bytes.Buffer > string add > fmt.Sprintf
為什么赵讯?
這里我們還是直接看源碼吧
先看 Sprintf
// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
這是 fmt.Sprintf 的源碼,我們可以看到內(nèi)部會(huì)通過(guò) newPrinter 創(chuàng)建一個(gè)新對(duì)象 p耿眉,點(diǎn)進(jìn)去看一下 newPrinter 這個(gè)函數(shù)
// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.fmt.init(&p.buf)
return p
}
它會(huì)從系統(tǒng)的臨時(shí)對(duì)象池中那 pp 這個(gè)對(duì)象边翼,關(guān)于臨時(shí)對(duì)象池(sync.Pool),下次有機(jī)會(huì)再探討鸣剪。這里可以知道组底, Sprintf 會(huì)從臨時(shí)對(duì)象池中獲取一個(gè) *pp 的指針丈积,然后再做一些格式化的操作,doPrintf 代碼就不貼了债鸡,格式化后的底層字節(jié)會(huì)放到 []byte 這個(gè)切片里面江滨,最后再 string 轉(zhuǎn)換成字符串返回,并且釋放掉 p 對(duì)象厌均。
整個(gè)過(guò)程:創(chuàng)建對(duì)象 - 格式化操作 - string化 - 釋放對(duì)象
接下來(lái)看 string
string 是在 Go 里面是一個(gè)不可變類型唬滑,所以下面的代碼
str = str + str2
每次都會(huì)創(chuàng)建一個(gè)新的 string 類型的值,然后重新賦值給 str 這個(gè)變量棺弊,相比于上面的 Sprintf 主要少了格式化這個(gè)過(guò)程晶密,所以在性能上肯定要優(yōu)于 Sprintf
bytes.Buffer
我們看一下 builder 的 String() 函數(shù)源碼
// String returns the contents of the unread portion of the buffer
// as a string. If the Buffer is a nil pointer, it returns "<nil>".
//
// To build strings more efficiently, see the strings.Builder type.
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
字符串的底層結(jié)構(gòu)是一個(gè) []byte 的字節(jié)序列,而 Buffer 是直接獲取未讀取的 []byte序列模她,在轉(zhuǎn)成 string 返回惹挟,少了重復(fù)創(chuàng)建對(duì)象這個(gè)步驟。
b.buf 是 []byte 切片
b.off 是已讀取的字節(jié)位置
strings.Builder
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
strings.Builder 直接通過(guò)指針來(lái)操作了缝驳,在效率上更進(jìn)一步连锯。
總結(jié)
通過(guò)源碼來(lái)分析,還是比較清晰明了的用狱,但是限于我自身的水平运怖,對(duì)于源碼的解讀并不都是特別深入,這里也是給大家做出一個(gè)參考夏伊。關(guān)于最后的通過(guò)轉(zhuǎn)換成指針來(lái)返回字符串的操作摇展,我也就是知道轉(zhuǎn)成指針效率會(huì)高,但是關(guān)于為什么溺忧,也都是模棱兩可(是因?yàn)橹苯油ㄟ^(guò)操作內(nèi)存地址嗎)咏连。總之關(guān)于基礎(chǔ)性鲁森、底層的東西還是要多多學(xué)習(xí)祟滴。