1 概述
C語言中字符串是使用字符數組char[]表示,數組的最後一位是\0
用來作爲字符串的定界符。Go語言中字符串最底層也是字符數組,但是Go語言使用長度來記錄字符串邊界,字符串的長度就是數組大小。
2 數據結構
Go語言字符串底層數據結構是reflect.StringHeader
(reflect/value.go),其中包含了指向字節數組的指針,以及數組大小:
type StringHeader struct {
Data uintptr
Len int
}
當一個字符串變量賦值給另外一個變量時候,他們StringHeader.Data
都指向同一個內存地址:
a := "hello"
b := a
從上圖中我們可以看到a變量和b變量的Data字段存儲的都是0x1234,而0x1234是字符數組的起始地址。
reflect.StringHeader 一共佔用16個字節空間,對於[n]string的大小,計算僞代碼如下:
unsafe.Sizeof([n]string) == n * 16
其中 unsafe.Sizeof() 是Go 中用來確定一個類型變量佔用空間大小的函數,這個大小是不含它引用的內存大小。比如某結構體中一個字段是個指針類型,這個字段指向的內存是不計算進去的,只會計算該字段本身的大小。
3 字符串拼接
3.1 + 號
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
strSlices := []string{"h", "e", "l", "l", "o"}
var all string
for _, str := range strSlices {
all += str
sh := (*reflect.StringHeader)(unsafe.Pointer(&all))
fmt.Printf("str地址:%p,all地址:%p,all底層字節數組地址=0x%x\n", &str, &all, sh.Data)
}
}
輸出
str地址:0xc000014270,all地址:0xc000014260,all底層字節數組地址=0x10a6eb1
str地址:0xc000014270,all地址:0xc000014260,all底層字節數組地址=0xc0000240a0
str地址:0xc000014270,all地址:0xc000014260,all底層字節數組地址=0xc0000240b0
str地址:0xc000014270,all地址:0xc000014260,all底層字節數組地址=0xc0000240c0
str地址:0xc000014270,all地址:0xc000014260,all底層字節數組地址=0xc0000240d0
可以發現str和all地址一直沒有變,但是all底層字節數組地址一直變化這說明拼接符在拼接字符串時候,會創建許多臨時字符串,這會造成浪費,並且也伴隨着內存分配。
3.2 bytes.Buffer拼接
package main
import "bytes"
func main() {
strSlices := []string{"h", "e", "l", "l", "o"}
var bf bytes.Buffer
for _, str := range strSlices {
bf.WriteString(str)
}
print(bf.String())
}
bytes.Buffer
底層結構包含內存緩衝,最少緩衝大小是614個字節,當進行字符串拼接時候,由於利用到了緩衝,拼接效率相比拼接符大大提升:
type Buffer struct {
buf []byte // 內存緩衝是字節切片類型
off int // buf已讀索引,下次讀取從buf[off]開始
lastRead readOp
}
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
提示:bytes.Buffer是可以複用的。當進行reset時候,並不會銷燬內存緩衝。
3.3 strings.Builder拼接
package main
import "strings"
func main() {
strSlices := []string{"h", "e", "l", "l", "o"}
var strb strings.Builder
for _, str := range strSlices {
strb.WriteString(str)
}
print(strb.String())
}
strings.Builder同bytes.Buffer一樣都是用內存緩衝,最大限度地減少了內存複製:
type Builder struct {
addr *Builder // 用來運行時檢測是否違背nocopy機制
buf []byte // 內存緩衝,類型是字節數組
}
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
從上面可以看到string.Builder的String方法使用unsafe.Pointer將字節數組轉換成字符串。而bytes.Buffer的String方法使用的string([]byte)將字節數組轉換成字符串,後者由於涉及內存分配和拷貝,相比執行效率低,具體可以參見[]byte(string) 和 string([]byte)爲什麼需要進行內存拷貝?。
3.4 基準測試
創建一個 strings_test.go 的文件,內容如下
package main
import (
"bytes"
"strings"
"testing"
)
// 使用+拼接符拼接字符串
func BenchmarkJoinStringUsePlus(b *testing.B) {
strSlices := []string{"h", "e", "l", "l", "o"}
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
var all string
for _, str := range strSlices {
all += str
}
_ = all
}
}
}
// 複用bytes.Buffer結構
func BenchmarkJoinStringUseBytesBufWithReuse(b *testing.B) {
strSlices := []string{"h", "e", "l", "l", "o"}
var bf bytes.Buffer
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
var all string
for _, str := range strSlices {
bf.WriteString(str)
}
all = bf.String()
_ = all
bf.Reset()
}
}
}
// 使用bytes.Buffer,未進行復用
func BenchmarkJoinStringUseBytesBufWithoutReuse(b *testing.B) {
strSlices := []string{"h", "e", "l", "l", "o"}
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
var all string
var bf bytes.Buffer
for _, str := range strSlices {
bf.WriteString(str)
}
all = bf.String()
_ = all
bf.Reset()
}
}
}
// 使用strings.Builder
func BenchmarkJoinStringUseStringBuilder(b *testing.B) {
strSlices := []string{"h", "e", "l", "l", "o"}
for i := 0; i < b.N; i++ {
for j := 0; j < 10000; j++ {
all := ""
var strb strings.Builder
for _, str := range strSlices {
strb.WriteString(str)
}
all = strb.String()
_ = all
strb.Reset()
}
}
}
執行命令
go test -bench . -benchmem
// . 是匹配所需要測試的函數的正則,意思執行當前package下所有的以Benchmark開頭命名的函數
// -benchmem 是指查看測試內存
輸出
goos: darwin
goarch: amd614
pkg: test
cpu: VirtualApple @ 2.50GHz
BenchmarkJoinStringUsePlus-8 1335 876617 ns/op 160001 B/op 40000 allocs/op
BenchmarkJoinStringUseBytesBufWithReuse-8 4765 401514 ns/op 0 B/op 0 allocs/op
BenchmarkJoinStringUseBytesBufWithoutReuse-8 1860 629879 ns/op 614000 B/op 10000 allocs/op
BenchmarkJoinStringUseStringBuilder-8 2290 547902 ns/op 80000 B/op 10000 allocs/op
PASS
ok test 6.561s
從上面結果可以分析得到字符串拼接效率,strings.Builder的效率最高,拼接字符+效率最低:
strings.Builder > bytes.Buffer > 拼接字符+
但是由於bytes.Buffer可以複用,若在需要多次執行字符串拼接的場景下,推薦使用它。
拼接方式 | 優點 | 缺點 | 適用場景 |
+ | 使用起來最簡單 | 效率低,每次拼接會產生臨時字符串 | 少量字符串拼接 |
fmt.Printf() | 支持多種數據類型拼接 | 需要將字符串轉換成空接口類型,效率差 | 多種數據類型拼接 |
strings.Builder() | 效率高 | 每次Reset()之後,其底層緩衝會被清除 | 適合單次拼接,不適合複用 |
strings.Join() | 底層其實使用的是strings.Builder,效率高 | 適合字符串數組 | |
bytes.Buffer() | 效率高,reset()不會銷燬內存緩衝 | 適合可以複用,不適合單次 |