在golang中字符串是一種不可變的字節序列,它能夠包含任意的數據,包括0值字節,但主要是人類能夠閱讀的文本。golang中默認字符串被解讀爲utf-8編碼的Unicode碼點(文字符號)序列。golang
golang中字符串具備不可變性。例如windows
str := "hello 世界!" str[0] = 'L'
這種寫法會引發編譯錯誤:str[0] 不可賦值數組
字符串支持相似數組中分片的引用寫法:緩存
fmt.Println(str[:5]) // 輸出 hello fmt.Println(str[7:]) // 輸出 世界 fmt.Println(str[len(s)+1:) // 宕機
str[i:j] , 當i、j 越界 (j 、i < 0 或 j、i > len(str) )或 j < i 時會發生宕機。安全
str := "Hello" t := str str += "world"
這種寫法能夠經過,雖然str指向了一個新的字符串「Hello world」,但t指向的舊字符串仍然存在。
不可變意味着兩個字符串可以安全的共享同一段底層內存,是的複製任何長度字符串的開銷都低廉,相似的字符串s及其子串(s[n:])字符串的複製也開銷低廉。函數
這個問題乍一看十分的簡單,直接遍歷就行了:性能
str := "Hello 世界!" for i := 0; i < len(str) ; i++{ fmt.Printf(「%c \t」,str[i]) }
然而事實沒那麼簡單,其輸出結果以下:測試
H e l l o ä ¸ å ½ ï ¼
中文字符部分所有爲亂碼。這與utf-8的編碼規則有關, utf-8是以字節爲單位對unicode碼點進行變長編碼。每個文字符號用1~4個字節表示,ASCII字符僅僅佔1字節的內存,其餘經常使用的文書符號會佔到2~3個字節。一個字符編碼的首字節高位指明後面還有多少個字節:優化
規則 | 表示範圍 | 說明 |
---|---|---|
0xxxxxxx | 文字符號0~127 | Ascii 字符 |
110xxxxx 10xxxxxx | 128~2047 | 少於128個未使用的值 |
1110xxxxx 10xxxxxx 10xxxxxx | 2048~65535 | 少於2048個未使用的值 |
1110xxxxx 10xxxxxx 10xxxxxx 10xxxxxx | 65536~0x10ffffff | 其餘未使用值 |
在上面提到的例子裏面Hello子串中的字符爲Ascii字符,佔用一個字節, 而 世界這兩個符號佔用的字符爲 3 個,因此遍歷的時候會出現亂碼的狀況。編碼
咱們這裏換一種寫法:
for i , v := range str { fmt.Printf("%d\t%c\n", i , v) }
其顯示結果以下:
0 H 1 e 2 l 3 l 4 o 5 6 中 9 國 12 !
以上代碼正常的輸出了每個字符包括中文字符。爲何使用range會成功? 由於range在循環的同時進行了隱式的解碼。其中 i 表示該字符在字符串中起始的下角標,v要重點說一下,它表示的是字符對應的unicode 點碼,在golang中它有一個專門的變量類型 rune (文字符號) ,它是int32類型的別名。在golang中int佔用內存大小取決於操做系統底層和計算機硬件,但int32必定是佔用 4 bytes ,rune類型在打印輸出的時候使用「%c」。
有了能遍歷輸出的函數天然很容易的就能夠寫出取反函數:
func Reverse(str string)(res string) { for _, v := range str { res = string(v) + res } return }
咱們測試一下這個函數的性能:
func BenchmarkReverse1(b *testing.B) { tStr := "Hello 中國!" for i := 0; i < b.N; i++{ Reverse(tStr) } }
做者在winx10/arm64 的操做系統中進行測試,cpu 爲core i3,內存爲 4g(硬件設施比較老舊了),最後得出的分析結果以下:
goos: windows goarch: amd64 pkg: project/learn/chapeter2 BenchmarkReverse1-4 2000000 788 ns/op PASS ROUTINE ======================== project/learn/chapeter2.Reverse1 in D:\gopath\src\project\learn\chapeter2\str.go 250ms 2.25s (flat, cum) 94.54% of Total . . 14: . . 15: res = string(rnStr) . . 16: return . . 17:} . . 18: 20ms 20ms 19:func Reverse1(str string)(res string) { . . 20: 80ms 110ms 21: for _, v := range str { 150ms 2.12s 22: res = string(v) + res . . 23: //res = fmt.Sprintf("%c%s", v ,res) . . 24: } . . 25: . . 26: return . . 27:}
能夠看見最耗時的操做就是res 從新賦值的部分,此時有兩種狀況:一、res字符串執行 + 操做很費時; 二、進行字符轉化的時候費時,咱們把代碼調整一下:
func Reverse(str string)(res string) { for _, v := range str { temp := string(v) res = temp + res } return }
性能測試結果以下
ROUTINE ======================== project/learn/chapeter2.Reverse1 in D:\gopath\src\project\learn\chapeter2\str.go 230ms 2.31s (flat, cum) 93.90% of Total . . 14: . . 15: res = string(rnStr) . . 16: return . . 17:} . . 18: 10ms 10ms 19:func Reverse1(str string)(res string) { 90ms 210ms 20: for _, v := range str { 30ms 200ms 21: temp := string(v) 90ms 1.88s 22: res = temp + res . . 23: . . 24: } . . 25: 10ms 10ms 26: return . . 27:} . . 28:
可見res 執行 + 操做要更費時一些,在執行+操做的過程當中,要經歷 字符串拷貝、底層字節數組內存從新分配(可能被觸發)。
優化的思路很簡單,建立一片‘緩存’,用來存儲字符串對應的字節數據,最後再統一轉化爲字符串。
func Reverse(str string)(res string) { i:=0 cache := make([]byte, len(str)) for _, v := range str { i += utf8.RuneLen(v) utf8.EncodeRune(cache[len(str) - i:], v) } res = string(cache) return }
執行結果以下:
goos: windows goarch: amd64 pkg: project/learn/chapeter2 BenchmarkReverse2-4 5000000 253 ns/op PASS ok project/learn/chapeter2 1.831s ROUTINE ======================== project/learn/chapeter2.Reverse2 in D:\gopath\src\project\learn\chapeter2\str.go 510ms 1.45s (flat, cum) 90.62% of Total . . 29: 20ms 20ms 30:func Reverse2(str string)(res string) { . . 31: i:=0 20ms 210ms 32: cache := make([]byte, len(str)) . . 33: 280ms 460ms 34: for _, v := range str { 50ms 90ms 35: i += utf8.RuneLen(v) 100ms 250ms 36: utf8.EncodeRune(cache[len(str) - i:], v) . . 37: } . . 38: 20ms 400ms 39: res = string(cache) 20ms 20ms 40: return . . 41:}
優化率接近68%。從以上過程咱們能夠對golang的字符串類型的變量有一個直觀的認識。