天天看點

《快學 Go 語言》第 7 課 —— 冰糖葫蘆串

字元串通常有兩種設計,一種是「字元」串,一種是「位元組」串。「字元」串中的每個字都是定長的,而「位元組」串中每個字是不定長的。Go 語言裡的字元串是「位元組」串,英文字元占用 1 個位元組,非英文字元占多個位元組。這意味着無法通過位置來快速定位出一個完整的字元來,而必須通過周遊的方式來逐個擷取單個字元。

我們所說的字元通常是指 unicode 字元,你可以認為所有的英文和漢字在 unicode 字元集中都有一個唯一的整數編号,一個 unicode 通常用 4 個位元組來表示,對應的 Go 語言中的字元 rune 占 4 個位元組。在 Go 語言的源碼中可以找到下面這行代碼,rune 類型是一個衍生類型,它在記憶體裡面使用 int32 類型的 4 個位元組存儲。

type rune int32
           

使用「字元」串來表示字元串勢必會浪費空間,因為所有的英文字元本來隻需要 1 個位元組來表示,用 rune 字元來表示的話那麼剩餘的 3 個位元組都是零。但是「字元」串有一個好處,那就是可以快速定位。

為了進一步友善讀者了解位元組 byte 和 字元 rune 的關系,我花了下面這張圖

圖檔

其中 codepoint 是每個「字」的其實偏移量。Go 語言的字元串采用 utf8 編碼,中文漢字通常需要占用 3 個位元組,英文隻需要 1 個位元組。len() 函數得到的是位元組的數量,通過下标來通路字元串得到的是「位元組」。

按位元組周遊

字元串可以通過下标來通路内部位元組數組具體位置上的位元組,位元組是 byte 類型

package main

import "fmt"

func main() {
var s = "嘻哈china"
for i:=0;i<len(s);i++ {

fmt.Printf("%x ", s[i])

}


}


-----------

e5 98 bb e5 93 88 63 68 69 6e 61
           

按字元 rune 周遊

package main

import "fmt"

func main() {
var s = "嘻哈china"
for codepoint, runeValue := range s {

fmt.Printf("%d %d ", codepoint, int32(runeValue))

}

}


-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
           

對字元串進行 range 周遊,每次疊代出兩個變量 codepoint 和 runeValue。codepoint 表示字元起始位置,runeValue 表示對應的 unicode 編碼(類型是 rune)。

位元組串的記憶體表示

如果字元串僅僅是位元組數組,那字元串的長度資訊是怎麼得到呢?要是字元串都是字面量的話,長度尚可以在編譯期計算出來,但是如果字元串是運作時構造的,那長度又是如何得到的呢?

var s1 = "hello" // 靜态字面量
var s2 = ""
for i:=0;i<10;i++ {

s2 += s1 // 動态構造

}

fmt.Println(len(s1))

fmt.Println(len(s2))
           

為解釋這點,就必須了解字元串的記憶體結構,它不僅僅是前面提到的那個位元組數組,編譯器還為它配置設定了頭部字段來存儲長度資訊和指向底層位元組數組的指針,圖示如下,結構非常類似于切片,差別是頭部少了一個容量字段。

當我們将一個字元串變量指派給另一個字元串變量時,底層的位元組數組是共享的,它隻是淺拷貝了頭部字段。

字元串是隻讀的

你可以使用下标來讀取字元串指定位置的位元組,但是你無法修改這個位置上的位元組内容。如果你嘗試使用下标指派,編譯器在文法上直接拒絕你。

package main

func main() {
var s = "hello"

s[0] = 'H'

}

--------

./main.go:5:7: cannot assign to s[0]
           

切割切割

字元串在記憶體形式上比較接近于切片,它也可以像切片一樣進行切割來擷取子串。子串和母串共享底層位元組數組。

package main

import "fmt"

func main() {
var s1 = "hello world"
var s2 = s1[3:8]

fmt.Println(s2)

}


-------

lo wo
           

位元組切片和字元串的互相轉換

在使用 Go 語言進行網絡程式設計時,經常需要将來自網絡的位元組流轉換成記憶體字元串,同時也需要将記憶體字元串轉換成網絡位元組流。Go 語言直接内置了位元組切片和字元串的互相轉換文法。

package main

import "fmt"

func main() {
var s1 = "hello world"
var b = []byte(s1) // 字元串轉位元組切片
var s2 = string(b) // 位元組切片轉字元串

fmt.Println(b)

fmt.Println(s2)

}


--------

[104 101 108 108 111 32 119 111 114 108 100]

hello world
           

從節省記憶體的角度出發,你可能會認為位元組切片和字元串的底層位元組數組是共享的。但是事實不是這樣的,底層位元組數組會被拷貝。如果内容很大,那麼轉換操作是需要一定成本的。

那為什麼需要拷貝呢?因為位元組切片的底層數組内容是可以修改的,而字元串的底層位元組數組是隻讀的,如果共享了,就會導緻字元串的隻讀屬性不再成立。

原文釋出時間為: 2018-11-20

本文作者:碼洞

本文來自雲栖社群合作夥伴“

碼洞

”,了解相關資訊可以關注“

”。

繼續閱讀