go-string

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()不會銷燬內存緩衝   適合可以複用,不適合單次

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章