這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang 變數在記憶體的形式
int uint 在不同系統不同編譯器有不同表現,gc 、gccgo 的實現是在 64 位元系統下,int uint 為 64 位元,而 32 位系統為 32 位。
類似的,指標長度在 64 位元系統為 8 位元組,32 位系統為 4 位元組。
數組、結構體中資料在記憶體中的緊密相連的。
字串
type stringStruct struct { str unsafe.Pointer len int}
字串使用 16 位元組長的資料結構表示,包含一個指向字串儲存資料的指標和一個長度資料。採用字串切片產生新的字串的時候不會涉及到記憶體的分配和複製操作,因為多個字串重用了底層的儲存資料,因為字串是不可變的(改變字串會產生新的字串),不會有記憶體共用問題。
Go 使用 utf-8 編碼字串,(utf-8編碼作者是 Go 作者之一),Go 的字串每一個字元是 rune,rune 是 uint32 的別名,unicode 字元的長度可能是1,2,3,4個位元組。如果統計字數算的是 rune。
s := "劉曦光"len(s) // 9,位元組數len([]rune(s)) // 3,rune 數
使用下標訪問字串,得到的不是第 n 個字元,而是底層儲存的第 n 個 byte。
s := "劉曦光"s[0] // 229
一個一直沒弄清楚的問題:Unicode UTF-8 string 之間的關係
上古時代的程式員可能會出現字元集、編碼方式等問題,但是現在我們開發中編碼方式等問題一般都有編輯器或者IDE提供好了完美的支援。
通過閱讀以下兩篇文章我弄清楚了二者的關係:
- 字元編碼筆記:ASCII,Unicode 和 UTF-8
- The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
簡而言之:
- Unicode 是 charset 字元集,for see 對應的是碼位/碼點/code point
- UTF-8 是 encoding 編碼方式,for store儲存在存放裝置上,記憶體外存
Unicode 字元集可以表示所有的字元,但是其有不同的實現方式,比如 UTF-18,UTF-16 等等。沒錯 UTF-8 只是 Unicode 的一種實現方式。UTF-8 採用特殊的編碼方式,使用頻率高的字元對應的儲存位元組數越短。
錯誤例子
之前學習 Java 的過程中閱讀過某些錯誤的資料說 Java 使用 Unicode 編碼,每一個字元採用兩個位元組儲存,可以表達所有的字元。
事實上,兩個位元組 16 位一共可表達的字元數量為 2 ** 32 = 65536,根本不足以表達所有字元,且 Unicode 只是字元集而不是編碼方式。Unicode 編碼數量沒有實際上限,事實上他們擁有遠超 65536 個,所以不是所有 Unicode 編碼都能夠被壓縮到 2 位元組。
即使 Times New Roman 等使用了不同樣式顯示 A,但是 A 還是同一個字元。只是使用了不同字型樣式顯示,儲存還是使用了相同的編碼方式。
在不同的系統中可能會有大端儲存 big-endian 或者小端儲存 small-endian,更多關於該方面可以閱讀阮一峰的一篇文章:理解位元組序
In Go a string is in effect a read-only slice of bytes.
unsafe.Sizeof(variable)
十六進位代表字串
s := "\x68\x65\x6c\x6c\x6f" // "hello"
數字中使用 0xFF 代表十六進位,0111 代表八進位
fmt.Printf("%q", string(100))
輸出的是 "d",而不是 "100"
Go 源碼 source code 只允許使用 UTF-8 編碼
使用 raw string 裡面不對 \n、 \xff 等進行轉義
s := `hello world`
Go 中的 string 只能夠包含 UTF-8 編碼嗎?
不是的,string 還能夠通過 "\xff" 等形式控制每一個 byte。
for range 遍曆字串中的每一個字元,而不是位元組
s := "hello world"for _, v := range s {// v 的類型是 int32,也就是 rune fmt.Println("%v ", v)}fmt.Println()for _, v := range s { fmt.Printf("%v ", string(v))}
output:
104 101 108 108 111 32 119 111 114 108 100h e l l o w o r l d
官方庫 unicode/utf8 中有很多 UTF-8 方面的支援
字串引用同一個源字串的壞處:對於一個很大的源字串,即使只有一小部分還被引用,源字串就無法被回收。
字串引用同一個源字串的好處:字串的切割、複製操作非常昂貴,需要分為分配-複製兩步。
Golang 官方對 strings 說明
slice
type slice struct { array unsafe.Pointer len int cap int}
數組、slice 並不會真正複製一份資料,而是複用了底層的數組儲存
即使是 slice 的賦值,底層的數組都是使用同一個,其中一個的變化會引發另外一個的同步變化
func main() {x := []int{1, 2, 3, 4, 5}y := x y[0] = 10 fmt.Println("x:", x) fmt.Println("y:", y)}
output:
x: [10 2 3 4 5]y: [10 2 3 4 5]
擴容
在對 slice 進行 append 等操作可能會觸發 slice 的擴容
擴容規則:
- 如果當前 cap < 1024,按每次 2 倍增長,否則每次按當前 cap 的 1/4 增長
建立 slice
可以通過 new 或 make 建立 slice,new 返回的是一個已經清零的指標,而 make 返回的是一個複雜的結構。
建立 slice 最好使用 make 建立
更多請參考:深入解析 Go 中 Slice 底層實現
map
type hmap struct { count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // optional fields}
func main() {m1 := make(map[int]int)m2 := m1for i := 0; i < 1000; i++ { m1[i] = i }for k, _ := range m1 {_, ok := m2[k] fmt.Println(ok) }}
總是輸出 true。
map 進行的複製並不會重新分配空間,而是複用了底層的儲存儲存結構,即使是 m1 插入了很多資料,已經觸發了擴充,buckets 的重新分配,m1 和 m2 還是會同步變化的。
map 使用鏈表解決雜湊衝突問題,而不是開放地址,因為開放地址法在真實擴容的時候效能下降得很快。鏈表的位置不需要重新計算雜湊值,因為擴容是成倍增長。
map 的擴容採用了兩個 bucket 的方法,不是一次性完成擴容操作,而不一次次地把 oldbuckets 中的元素移到 buckets 中,雖然這樣不能夠消除總擴充時間,但是擴充時間分攤到每一次插入,這樣防止程式發生長時間的阻塞。
更多請參考:
- 如何設計並實現一個安全執行緒的 Map ?(上篇)
- 如何設計並實現一個安全執行緒的 Map ?(下篇)