深度解密Go语言之Slice
Go 語言的?slice?很好用,不過也有一些坑。slice?是 Go 語言一個很重要的數據結構。網上已經有很多文章寫過了,似乎沒必要再寫。但是每個人看問題的視角不同,寫出來的東西自然也不一樣。我這篇會從更底層的匯編語言去解讀它。而且在我寫這篇文章的過程中,發現絕大部分文章都存在一些問題,文章里會講到,這里先不展開。
我希望本文可以終結這個話題,下次再有人想和你討論?slice,直接把這篇文章的鏈接丟過去就行了。
按照慣例,為了更好的閱讀體驗,手動貼上文章的目錄:
當我們在說 slice 時,到底在說什么
slice?翻譯成中文就是切片,它和數組(array)很類似,可以用下標的方式進行訪問,如果越界,就會產生 panic。但是它比數組更靈活,可以自動地進行擴容。
了解 slice 的本質,最簡單的方法就是看它的源代碼:
// runtime/slice.go type slice struct { array unsafe.Pointer // 元素指針 len int // 長度 cap int // 容量 }看到了嗎,slice?共有三個屬性:?指針,指向底層數組;?長度,表示切片可用元素的個數,也就是說使用下標對 slice 的元素進行訪問時,下標不能超過 slice 的長度;?容量,底層數組的元素個數,容量 >= 長度。在底層數組不進行擴容的情況下,容量也是 slice 可以擴張的最大限度。
注意,底層數組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進行操作是有可能影響到其他 slice 的。
slice 的創建
創建 slice 的方式有以下幾種:
| 序號 | 方式 | 代碼示例 |
| 1 | 直接聲明 | var slice []int |
| 2 | new | slice := *new([]int) |
| 3 | 字面量 | slice := []int{1,2,3,4,5} |
| 4 | make | slice := make([]int, 5, 10) |
| 5 | 從切片或數組“截取” | slice := array[1:5]?或?slice := sourceSlice[1:5] |
直接聲明
第一種創建出來的 slice 其實是一個?nil slice。它的長度和容量都為0。和nil比較的結果為true。
這里比較混淆的是empty slice,它的長度和容量也都為0,但是所有的空切片的數據指針都指向同一個地址?0xc42003bda0。空切片和?nil?比較的結果為false。
它們的內部結構如下圖:
| 創建方式 | nil切片 | 空切片 |
| 方式一 | var s1 []int | var s2 = []int{} |
| 方式二 | var s4 = *new([]int) | var s3 = make([]int, 0) |
| 長度 | 0 | 0 |
| 容量 | 0 | 0 |
| 和?nil?比較 | true | false |
nil?切片和空切片很相似,長度和容量都是0,官方建議盡量使用?nil?切片。
關于nil slice和empty slice的探索可以參考公眾號“碼洞”作者老錢寫的一篇文章《深度解析 Go 語言中「切片」的三種特殊狀態》,地址附在了參考資料部分。
字面量
比較簡單,直接用初始化表達式創建。
運行結果:
[0 1 2 3 0 0 0 0 100] 9 9唯一值得注意的是上面的代碼例子中使用了索引號,直接賦值,這樣,其他未注明的元素則默認?0 值。
make
make函數需要傳入三個參數:切片類型,長度,容量。當然,容量可以不傳,默認和長度相等。
上篇文章《走進Go的底層》中,我們學到了匯編這個工具,這次我們再次請出匯編來更深入地看看slice。如果沒看過上篇文章,建議先回去看完,再繼續閱讀本文效果更佳。
先來一小段玩具代碼,使用?make?關鍵字創建?slice:
執行如下命令,得到 Go 匯編代碼:
go tool compile -S main.go我們只關注main函數:
0x0000 00000 (main.go:5)TEXT "".main(SB), $96-0 0x0000 00000 (main.go:5)MOVQ (TLS), CX 0x0009 00009 (main.go:5)CMPQ SP, 16(CX) 0x000d 00013 (main.go:5)JLS 228 0x0013 00019 (main.go:5)SUBQ $96, SP 0x0017 00023 (main.go:5)MOVQ BP, 88(SP) 0x001c 00028 (main.go:5)LEAQ 88(SP), BP 0x0021 00033 (main.go:5)FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0021 00033 (main.go:5)FUNCDATA $1, gclocals·57cc5e9a024203768cbab1c731570886(SB) 0x0021 00033 (main.go:5)LEAQ type.int(SB), AX 0x0028 00040 (main.go:6)MOVQ AX, (SP) 0x002c 00044 (main.go:6)MOVQ $5, 8(SP) 0x0035 00053 (main.go:6)MOVQ $10, 16(SP) 0x003e 00062 (main.go:6)PCDATA $0, $0 0x003e 00062 (main.go:6)CALL runtime.makeslice(SB) 0x0043 00067 (main.go:6)MOVQ 24(SP), AX 0x0048 00072 (main.go:6)MOVQ 32(SP), CX 0x004d 00077 (main.go:6)MOVQ 40(SP), DX 0x0052 00082 (main.go:7)CMPQ CX, $2 0x0056 00086 (main.go:7)JLS 221 0x005c 00092 (main.go:7)MOVQ $2, 16(AX) 0x0064 00100 (main.go:8)MOVQ AX, ""..autotmp_2+64(SP) 0x0069 00105 (main.go:8)MOVQ CX, ""..autotmp_2+72(SP) 0x006e 00110 (main.go:8)MOVQ DX, ""..autotmp_2+80(SP) 0x0073 00115 (main.go:8)MOVQ $0, ""..autotmp_1+48(SP) 0x007c 00124 (main.go:8)MOVQ $0, ""..autotmp_1+56(SP) 0x0085 00133 (main.go:8)LEAQ type.[]int(SB), AX 0x008c 00140 (main.go:8)MOVQ AX, (SP) 0x0090 00144 (main.go:8)LEAQ ""..autotmp_2+64(SP), AX 0x0095 00149 (main.go:8)MOVQ AX, 8(SP) 0x009a 00154 (main.go:8)PCDATA $0, $1 0x009a 00154 (main.go:8)CALL runtime.convT2Eslice(SB) 0x009f 00159 (main.go:8)MOVQ 16(SP), AX 0x00a4 00164 (main.go:8)MOVQ 24(SP), CX 0x00a9 00169 (main.go:8)MOVQ AX, ""..autotmp_1+48(SP) 0x00ae 00174 (main.go:8)MOVQ CX, ""..autotmp_1+56(SP) 0x00b3 00179 (main.go:8)LEAQ ""..autotmp_1+48(SP), AX 0x00b8 00184 (main.go:8)MOVQ AX, (SP) 0x00bc 00188 (main.go:8)MOVQ $1, 8(SP) 0x00c5 00197 (main.go:8)MOVQ $1, 16(SP) 0x00ce 00206 (main.go:8)PCDATA $0, $1 0x00ce 00206 (main.go:8)CALL fmt.Println(SB) 0x00d3 00211 (main.go:9)MOVQ 88(SP), BP 0x00d8 00216 (main.go:9)ADDQ $96, SP 0x00dc 00220 (main.go:9)RET 0x00dd 00221 (main.go:7)PCDATA $0, $0 0x00dd 00221 (main.go:7)CALL runtime.panicindex(SB) 0x00e2 00226 (main.go:7)UNDEF 0x00e4 00228 (main.go:7)NOP 0x00e4 00228 (main.go:5)PCDATA $0, $-1 0x00e4 00228 (main.go:5)CALL runtime.morestack_noctxt(SB) 0x00e9 00233 (main.go:5)JMP 0先說明一下,Go 語言匯編?FUNCDATA?和?PCDATA?是編譯器產生的,用于保存一些和垃圾收集相關的信息,我們先不用 care。
以上匯編代碼行數比較多,沒關系,因為命令都比較簡單,而且我們的 Go 源碼也足夠簡單,沒有理由看不明白。
我們先從上到下掃一眼,看到幾個關鍵函數:
CALL runtime.makeslice(SB) CALL runtime.convT2Eslice(SB) CALL fmt.Println(SB) CALL runtime.morestack_noctxt(SB)| 序號 | 功能 |
| 1 | 創建slice |
| 2 | 類型轉換 |
| 3 | 打印函數 |
| 4 | 棧空間擴容 |
1是創建 slice 相關的;2是類型轉換;調用?fmt.Println需要將 slice 作一個轉換;?3是打印語句;4是棧空間擴容函數,在函數開始處,會檢查當前棧空間是否足夠,不夠的話需要調用它來進行擴容。暫時可以忽略。
調用了函數就會涉及到參數傳遞,Go 的參數傳遞都是通過 棧空間完成的。接下來,我們詳細分析這整個過程。
| 行數 | 作用 |
| 1 | main函數定義,棧幀大小為?96B |
| 2-4 | 判斷棧是否需要進行擴容,如果需要則跳到?228,這里會調用?runtime.morestack_noctxt(SB)?進行棧擴容操作。具體細節后續還會有文章來講 |
| 5-9 | 將?caller BP?壓棧,具體細節后面會講到 |
| 10-15 | 調用?runtime.makeslice(SB)?函數及準備工作。*_type表示的是?int,也就是?slice?元素的類型。這里對應的源碼是第6行,也就是調用?make?創建?slice?的那一行。5?和?10?分別代表長度和容量,函數參數會在棧頂準備好,之后執行函數調用命令?CALL,進入到被調用函數的棧幀,就會按順序從?caller?的棧頂取函數參數 |
| 16-18 | 接收?makeslice的返回值,通過?move?移動到寄存器中 |
| 19-21 | 給數組索引值為?2?的元素賦上值?2,因為是?int?型的?slice,元素大小為8字節,所以?MOVQ $2, 16(AX)?此命令就是將?2?搬到索引為?2?的位置。這里還會對索引值的大小進行檢查,如果越界,則會跳轉到?221,執行?panic?函數 |
| 22-26 | 分別通過寄存器?AX,CX,DX?將?makeslice?的返回值?move?到內存的其他位置,也稱為局部變量,這樣就構造出了?slice |
左邊是棧上的數據,右邊是堆上的數據。array?指向?slice?的底層數據,被分配到堆上了。注意,棧上的地址是從高向低增長;堆則從低向高增長。棧左邊的數字表示對應的匯編代碼的行數,棧右邊箭頭則表示棧地址。(48)SP、(56)SP 表示的內容接著往下看。
注意,在圖中,棧地址是從下往上增長,所以 SP 表示的是圖中?*_type?所在的位置,其它的依此類推。
| 行數 | 作用 |
| 27-32 | 準備調用?runtime.convT2Eslice(SB)的函數參數 |
| 33-36 | 接收返回值,通過AX,CX寄存器?move?到(48)SP、(56)SP |
convT2Eslice?的函數聲明如下:
func convT2Eslice(t *_type, elem unsafe.Pointer) (e eface)第一個參數是指針?*_type,_type是一個表示類型的結構體,這里傳入的就是?slice的類型?[]int;第二個參數則是元素的指針,這里傳入的就是?slice?底層數組的首地址。
返回值?eface?的結構體定義如下:
type eface struct { _type *_type data unsafe.Pointer }由于我們會調用?fmt.Println(slice),看下函數原型:
func Println(a ...interface{}) (n int, err error)Println?接收 interface 類型,因此我們需要將?slice?轉換成 interface 類型。由于?slice?沒有方法,是個“空 interface”。因此會調用?convT2Eslice?完成這一轉換過程。
convT2Eslice?函數返回的是類型指針和數據地址。源碼就不貼了,大體流程是:調用?mallocgc?分配一塊內存,把數據?copy?進到新的內存,然后返回這塊內存的地址,*_type?則直接返回傳入的參數。
32(SP)?和?40(SP)?其實是?makeslice?函數的返回值,這里可以忽略。
還剩?fmt.Println(slice)?最后一個函數調用了,我們繼續。
| 行數 | 作用 |
| 37-40 | 準備?Println?函數參數。共3個參數,第一個是類型地址,還有兩個?1,這塊暫時還不知道為什么要傳,有了解的同學可以在文章后面留言 |
所以調用?fmt.Println(slice)?時,實際是傳入了一個?slice類型的eface地址。這樣,Println就可以訪問類型中的數據,最終給“打印”出來。
最后,我們看下?main?函數棧幀的開始和收尾部分。
0x0013 00019 (main.go:5)SUBQ $96, SP 0x0017 00023 (main.go:5)MOVQ BP, 88(SP) 0x001c 00028 (main.go:5)LEAQ 88(SP), BP ………………………… 0x00d3 00211 (main.go:9)MOVQ 88(SP), BP 0x00d8 00216 (main.go:9)ADDQ $96, SP RETBP可以理解為保存了當前函數棧幀棧底的地址,SP則保存棧頂的地址。
初始,BP?和?SP?分別有一個初始狀態。
main?函數執行的時候,先根據?main?函數棧幀大小確定?SP?的新指向,使得?main?函數棧幀大小達到?96B。之后把老的?BP?保存到?main?函數棧幀的底部,并使?BP?寄存器重新指向新的棧底,也就是?main?函數棧幀的棧底。
最后,當?main?函數執行完畢,把它棧底的?BP?給回彈回到?BP?寄存器,恢復調用前的初始狀態。一切都像是沒有發生一樣,完美的現場。
這部分,又詳細地分析了一遍函數調用的過程。一方面,讓大家復習一下上一篇文章講的內容;另一方面,向大家展示如何找到 Go 中的一個函數背后真實調用了哪些函數。像例子中,我們就看到了?make?函數背后,實際上是調用了?makeslice?函數;還有一點,讓大家對匯編不那么“懼怕”,可以輕松地分析一些東西。
截取
截取也是比較常見的一種創建 slice 的方法,可以從數組或者 slice 直接截取,當然需要指定起止索引位置。
基于已有 slice 創建新 slice 對象,被稱為?reslice。新 slice 和老 slice 共用底層數組,新老 slice 對底層數組的更改都會影響到彼此。基于數組創建的新 slice 對象也是同樣的效果:對數組或 slice 元素作的更改都會影響到彼此。
值得注意的是,新老 slice 或者新 slice 老數組互相影響的前提是兩者共用底層數組,如果因為執行?append?操作使得新 slice 底層數組擴容,移動到了新的位置,兩者就不會相互影響了。所以,問題的關鍵在于兩者是否會共用底層數組。
截取操作采用如下方式:
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} slice := data[2:4:6] // data[low, high, max]對?data?使用3個索引值,截取出新的slice。這里?data?可以是數組或者?slice。low?是最低索引值,這里是閉區間,也就是說第一個元素是?data?位于?low?索引處的元素;而?high?和?max?則是開區間,表示最后一個元素只能是索引?high-1?處的元素,而最大容量則只能是索引?max-1?處的元素。
當?high == low?時,新?slice?為空。
還有一點,high?和?max?必須在老數組或者老?slice?的容量(cap)范圍內。
來看一個例子,來自雨痕大佬《Go學習筆記》第四版,P43頁,參考資料里有開源書籍地址。這里我會進行擴展,并會作詳細說明:
package main import "fmt" func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] s2 := s1[2:6:7] s2 = append(s2, 100) s2 = append(s2, 200) s1[2] = 20 fmt.Println(s1) fmt.Println(s2) fmt.Println(slice) }先看下代碼運行的結果:
[2 3 20] [4 5 6 7 100 200] [0 1 2 3 20 5 6 7 100 9]我們來走一遍代碼,初始狀態如下:
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] s2 := s1[2:6:7]s1?從?slice?索引2(閉區間)到索引5(開區間,元素真正取到索引4),長度為3,容量默認到數組結尾,為8。?s2?從?s1?的索引2(閉區間)到索引6(開區間,元素真正取到索引5),容量到索引7(開區間,真正到索引6),為5。
接著,向?s2?尾部追加一個元素 100:
s2 = append(s2, 100)s2?容量剛好夠,直接追加。不過,這會修改原始數組對應位置的元素。這一改動,數組和?s1?都可以看得到。
再次向?s2?追加元素200:
s2 = append(s2, 100)這時,s2?的容量不夠用,該擴容了。于是,s2?另起爐灶,將原來的元素復制新的位置,擴大自己的容量。并且為了應對未來可能的?append?帶來的再一次擴容,s2?會在此次擴容的時候多留一些?buffer,將新的容量將擴大為原始容量的2倍,也就是10了。
最后,修改?s1?索引為2位置的元素:
s1[2] = 20這次只會影響原始數組相應位置的元素。它影響不到?s2?了,人家已經遠走高飛了。
再提一點,打印?s1?的時候,只會打印出?s1?長度以內的元素。所以,只會打印出3個元素,雖然它的底層數組不止3個元素。
至于,我們想在匯編層面看看到底它們是如何共享底層數組的,限于篇幅,這里不再展開。感興趣的同學可以在公眾號后臺回復:切片截取。
我會給你詳細分析函數調用關系,對共享底層數組的行為也會一目了然。
slice 和數組的區別在哪
slice 的底層數據是數組,slice 是對數組的封裝,它描述一個數組的片段。兩者都可以通過下標來訪問單個元素。
數組是定長的,長度定義好之后,不能再更改。在 Go 中,數組是不常見的,因為其長度是類型的一部分,限制了它的表達能力,比如?[3]int?和?[4]int?就是不同的類型。
而切片則非常靈活,它可以動態地擴容。切片的類型和長度無關。
append 到底做了什么
先來看看?append?函數的原型:
func append(slice []Type, elems ...Type) []Typeappend 函數的參數長度可變,因此可以追加多個值到 slice 中,還可以用?...?傳入 slice,直接追加一個切片。
slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...)append函數返回值是一個新的slice,Go編譯器不允許調用了 append 函數后不使用返回值。
append(slice, elem1, elem2) append(slice, anotherSlice...)所以上面的用法是錯的,不能編譯通過。
使用 append 可以向 slice 追加元素,實際上是往底層數組添加元素。但是底層數組的長度是固定的,如果索引?len-1?所指向的元素已經是底層數組的最后一個元素,就沒法再添加了。
這時,slice 會遷移到新的內存位置,新底層數組的長度也會增加,這樣就可以放置新增的元素。同時,為了應對未來可能再次發生的 append 操作,新的底層數組的長度,也就是新?slice?的容量是留了一定的?buffer?的。否則,每次添加元素的時候,都會發生遷移,成本太高。
新 slice 預留的?buffer?大小是有一定規律的。網上大多數的文章都是這樣描述的:
當原 slice 容量小于?1024?的時候,新 slice 容量變成原來的?2?倍;原 slice 容量超過?1024,新 slice 容量變成原來的1.25倍。
我在這里先說結論:以上描述是錯誤的。
為了說明上面的規律是錯誤的,我寫了一小段玩具代碼:
package main import "fmt" func main() { s := make([]int, 0) oldCap := cap(s) for i := 0; i < 2048; i++ { s = append(s, i) newCap := cap(s) if newCap != oldCap { fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }我先創建了一個空的?slice,然后,在一個循環里不斷往里面?append?新的元素。然后記錄容量的變化,并且每當容量發生變化的時候,記錄下老的容量,以及添加完元素之后的容量,同時記下此時?slice?里的元素。這樣,我就可以觀察,新老?slice?的容量變化情況,從而找出規律。
運行結果:
[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 1024 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304在老 slice 容量小于1024的時候,新 slice 的容量的確是老 slice 的2倍。目前還算正確。
但是,當老 slice 容量大于等于?1024?的時候,情況就有變化了。當向 slice 中添加元素?1280?的時候,老 slice 的容量為?1280,之后變成了?1696,兩者并不是?1.25?倍的關系(1696/1280=1.325)。添加完?1696?后,新的容量?2304?當然也不是?1696?的?1.25?倍。
可見,現在網上各種文章中的擴容策略并不正確。我們直接搬出源碼:源碼面前,了無秘密。
從前面匯編代碼我們也看到了,向 slice 追加元素的時候,若容量不夠,會調用?growslice?函數,所以我們直接看它的代碼。
// go 1.9.5 src/runtime/slice.go:82 func growslice(et *_type, old slice, cap int) slice { // …… newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for newcap < cap { newcap += newcap / 4 } } } // …… capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize) }看到了嗎?如果只看前半部分,現在網上各種文章里說的?newcap?的規律是對的。現實是,后半部分還對?newcap?作了一個內存對齊,這個和內存分配策略相關。進行內存對齊之后,新 slice 的容量是要?大于等于?老 slice 容量的?2倍或者1.25倍。
之后,向 Go 內存管理器申請內存,將老 slice 中的數據復制過去,并且將 append 的元素添加到新的底層數組中。
最后,向?growslice?函數調用者返回一個新的 slice,這個 slice 的長度并沒有變化,而容量卻增大了。
關于?append,我們最后來看一個例子,來源于參考資料部分的【Golang Slice的擴容規則】。
package main import "fmt" func main() { s := []int{1,2} s = append(s,4,5,6) fmt.Printf("len=%d, cap=%d",len(s),cap(s)) }運行結果是:
len=5, cap=6如果按網上各種文章中總結的那樣:小于原 slice 長度小于 1024 的時候,容量每次增加 1 倍。添加元素 4 的時候,容量變為4;添加元素 5 的時候不變;添加元素 6 的時候容量增加 1 倍,變成 8。
那上面代碼的運行結果就是:
len=5, cap=8這是錯誤的!我們來仔細看看,為什么會這樣,再次搬出代碼:
// go 1.9.5 src/runtime/slice.go:82 func growslice(et *_type, old slice, cap int) slice { // …… newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { // …… } // …… capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize) }這個函數的參數依次是?元素的類型,老的 slice,新 slice 最小求的容量。
例子中?s?原來只有 2 個元素,len?和?cap?都為 2,append?了三個元素后,長度變為 3,容量最小要變成 5,即調用?growslice?函數時,傳入的第三個參數應該為 5。即?cap=5。而一方面,doublecap?是原?slice容量的 2 倍,等于 4。滿足第一個?if?條件,所以?newcap?變成了 5。
接著調用了?roundupsize?函數,傳入 40。(代碼中ptrSize是指一個指針的大小,在64位機上是8)
我們再看內存對齊,搬出?roundupsize?函數的代碼:
// src/runtime/msize.go:13 func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) } else { //…… } } //…… } const _MaxSmallSize = 32768 const smallSizeMax = 1024 const smallSizeDiv = 8很明顯,我們最終將返回這個式子的結果:
class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]這是?Go?源碼中有關內存分配的兩個?slice。class_to_size通過?spanClass獲取?span劃分的?object大小。而?size_to_class8?表示通過?size?獲取它的?spanClass。
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31} var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}我們傳進去的?size?等于 40。所以?(size+smallSizeDiv-1)/smallSizeDiv = 5;獲取?size_to_class8?數組中索引為?5?的元素為?4;獲取?class_to_size?中索引為?4?的元素為?48。
最終,新的 slice 的容量為?6:
newcap = int(capmem / ptrSize) // 6至于,上面的兩個魔法數組的由來,暫時就不展開了。
為什么 nil slice 可以直接 append
其實?nil slice?或者?empty slice?都是可以通過調用 append 函數來獲得底層數組的擴容。最終都是調用?mallocgc?來向 Go 的內存管理器申請到一塊內存,然后再賦給原來的nil slice?或?empty slice,然后搖身一變,成為“真正”的?slice?了。
傳 slice 和 slice 指針有什么區別
前面我們說到,slice 其實是一個結構體,包含了三個成員:len, cap, array。分別表示切片長度,容量,底層數據的地址。
當 slice 作為函數參數時,就是一個普通的結構體。其實很好理解:若直接傳 slice,在調用者看來,實參 slice 并不會被函數中的操作改變;若傳的是 slice 的指針,在調用者看來,是會被改變原 slice 的。
值的注意的是,不管傳的是 slice 還是 slice 指針,如果改變了 slice 底層數組的數據,會反應到實參 slice 的底層數據。為什么能改變底層數組的數據?很好理解:底層數據在 slice 結構體里是一個指針,僅管 slice 結構體自身不會被改變,也就是說底層數據地址不會被改變。 但是通過指向底層數據的指針,可以改變切片的底層數據,沒有問題。
通過 slice 的 array 字段就可以拿到數組的地址。在代碼里,是直接通過類似?s[i]=10?這種操作改變 slice 底層數組元素值。
另外,啰嗦一句,Go 語言的函數參數傳遞,只有值傳遞,沒有引用傳遞。后面會再寫一篇相關的文章,敬請期待。
再來看一個年幼無知的代碼片段:
package main func main() { s := []int{1, 1, 1} f(s) fmt.Println(s) } func f(s []int) { // i只是一個副本,不能改變s中元素的值 /*for _, i := range s { i++ } */ for i := range s { s[i] += 1 } }運行一下,程序輸出:
[2 2 2]果真改變了原始 slice 的底層數據。這里傳遞的是一個 slice 的副本,在?f?函數中,s?只是?main?函數中?s?的一個拷貝。在f?函數內部,對?s?的作用并不會改變外層?main?函數的?s。
要想真的改變外層?slice,只有將返回的新的 slice 賦值到原始 slice,或者向函數傳遞一個指向 slice 的指針。我們再來看一個例子:
package main import "fmt" func myAppend(s []int) []int { // 這里 s 雖然改變了,但并不會影響外層函數的 s s = append(s, 100) return s } func myAppendPtr(s *[]int) { // 會改變外層 s 本身 *s = append(*s, 100) return } func main() { s := []int{1, 1, 1} newS := myAppend(s) fmt.Println(s) fmt.Println(newS) s = newS myAppendPtr(&s) fmt.Println(s) }運行結果:
[1 1 1] [1 1 1 100] [1 1 1 100 100]myAppend?函數里,雖然改變了?s,但它只是一個值傳遞,并不會影響外層的?s,因此第一行打印出來的結果仍然是?[1 1 1]。
而?newS?是一個新的?slice,它是基于?s?得到的。因此它打印的是追加了一個?100?之后的結果:?[1 1 1 100]。
最后,將?newS?賦值給了?s,s?這時才真正變成了一個新的slice。之后,再給?myAppendPtr?函數傳入一個?s 指針,這回它真的被改變了:[1 1 1 100 100]。
總結
到此,關于?slice?的部分就講完了,不知大家有沒有看過癮。我們最后來總結一下:
?切片是對底層數組的一個抽象,描述了它的一個片段。?切片實際上是一個結構體,它有三個字段:長度,容量,底層數據的地址。?多個切片可能共享同一個底層數組,這種情況下,對其中一個切片或者底層數組的更改,會影響到其他切片。?append?函數會在切片容量不夠的情況下,調用?growslice?函數獲取所需要的內存,這稱為擴容,擴容會改變元素原來的位置。?擴容策略并不是簡單的擴為原切片容量的?2?倍或?1.25?倍,還有內存對齊的操作。擴容后的容量 >= 原容量的?2?倍或?1.25?倍。?當直接用切片作為函數參數時,可以改變切片的元素,不能改變切片本身;想要改變切片本身,可以將改變后的切片返回,函數調用者接收改變后的切片或者將切片指針作為函數參數。
參考資料
【碼洞《深度解析 Go 語言中「切片」的三種特殊狀態》】https://juejin.im/post/5bea58df6fb9a049f153bca8
【老錢 數組】https://juejin.im/post/5be53bc251882516c15af2e0
【老錢 切片】https://juejin.im/post/5be8e0b1f265da614d08b45a
【golang interface源碼】https://i6448038.github.io/2018/10/01/Golang-interface/
【golang interface源碼】http://legendtkl.com/2017/07/01/golang-interface-implement/
【interface】https://www.jishuwen.com/d/2C9z#tuit
【雨痕開源Go學習筆記】https://github.com/qyuhen/book
【slice 圖很漂亮】https://halfrost.com/go_slice/
【Golang Slice的擴容規則】https://jodezer.github.io/2017/05/golangSlice%E7%9A%84%E6%89%A9%E5%AE%B9%E8%A7%84%E5%88%99
【slice作為參數】https://www.cnblogs.com/fwdqxl/p/9317769.html
【源碼】https://ictar.xyz/2018/10/25/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-go-slice/
【append機制 譯文】https://brantou.github.io/2017/05/24/go-array-slice-string/
【slice 匯編】http://xargin.com/go-slice/
【slice tricks】https://colobu.com/2017/03/22/Slice-Tricks/
【有圖】https://i6448038.github.io/2018/08/11/array-and-slice-principle/
【slice的本質】https://www.flysnow.org/2018/12/21/golang-sliceheader.html
【slice使用技巧】https://blog.thinkeridea.com/201901/go/slice_de_yi_xie_shi_yong_ji_qiao.html
【slice/array、內存增長】https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html
最后,如果你覺得本文對你有幫助的話,幫我點一下?在看?或者轉發到朋友圈,原創需要你的鼓勵和支持,感謝!
總結
以上是生活随笔為你收集整理的深度解密Go语言之Slice的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入Go的底层,带你走近一群有追求的人
- 下一篇: 深度解密Go语言之反射