GO語言slice詳解(結合原始碼)
一、GO語言中slice的定義
slice 是一種結構體型別,在原始碼中的定義為:
src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
從定義中可以看到,slice是一種值型別,裡面有3個元素。array是陣列指標,它指向底層分配的陣列;len是底層陣列的元素個數;cap是底層陣列的容量,超過容量會擴容。
二、初始化操作
slice有三種初始化操作,請看下面程式碼:
package main import "fmt" func main() { //1、make a := make([]int32, 0, 5) //2、[]int32{} b := []int32{1, 2, 3} //3、new([]int32) c := *new([]int32) fmt.Println(a, b, c) }
這幾種初始化方式,在底層實現是不一樣的。有一種瞭解底層實現好的方法,就是看反彙編的呼叫函式。執行下面命令即可看到程式碼某一行的反彙編:
go tool compile -S plan9Test.go | grep plan9Test.go:行號
1、make初始化
make函式初始化有三個引數,第一個是型別,第二個長度,第三個容量,容量要大於等於長度。slice的make初始化呼叫的是底層的runtime.makeslice
函式。
func makeslice(et *_type, len, cap int) slice { // NOTE: The len > maxElements check here is not strictly necessary, // but it produces a 'len out of range' error instead of a 'cap out of range' error // when someone does make([]T, bignumber). 'cap out of range' is true too, // but since the cap is only being supplied implicitly, saying len is clearer. // See issue 4085. maxElements := maxSliceCap(et.size) if len < 0 || uintptr(len) > maxElements { panic(errorString("makeslice: len out of range")) } if cap < len || uintptr(cap) > maxElements { panic(errorString("makeslice: cap out of range")) } p := mallocgc(et.size*uintptr(cap), et, true) return slice{p, len, cap} }
主要就是呼叫mallocgc
分配一塊 個數cap*型別大小 的記憶體給底層陣列,然後返回一個slice,slice的array指標指向分配的底層陣列。
2、[]int32{} 初始化
這種初始化底層是呼叫 runtime.newobject
函式直接分配相應個數的底層陣列。
// implementation of new builtin // compiler (both frontend and SSA backend) knows the signature // of this function func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
3、new([]int32) 初始化
這種初始化底層也是呼叫 runtime.newobject
,new是返回slice 的地址,所以要取地址裡面內容才是真正的slice。
三、reSlice(切片操作)
所謂reSlice,是基於已有 slice 建立新的 slice 物件,以便在容量cap允許範圍內調整屬性。
data := []int32{0,1,2,3,4,5,6}
slice := data[1:4:5] // [low:high:max]
切片操作有三個引數,low、high、max,新生成的 slice 結構體三個引數,指標array指向原slice 底層陣列元素下標為low的位置, len = high - low, cap = max - low。如下圖所示:
切片操作主要要注意的就是在原slice 容量允許範圍,超出容量範圍會報panic。
四、append 操作
slice 的 append 操作是向底層陣列尾部新增資料,返回 新的slice物件。
請看下面一段程式碼:
package main
import (
"fmt"
)
func main() {
a := make([]int32, 1, 2)
b := append(a, 1)
c := append(a, 1, 2)
fmt.Printf("a的地址:%p, 第一個元素地址:%p,容量:%v\n", &a, &a[0], cap(a))
//a的地址:0xc42000a060, 第一個元素地址:0xc42001a090,容量:2
fmt.Printf("b的地址:%p, 第一個元素地址:%p,容量:%v\n", &b, &b[0], cap(b))
//b的地址:0xc42000a080, 第一個元素地址:0xc42001a090,容量:2
fmt.Printf("c的地址:%p, 第一個元素地址:%p,容量:%v\n", &c, &c[0], cap(c))
//c的地址:0xc42000a0a0, 第一個元素地址:0xc42001a0a0,容量:4
}
從上面程式碼的列印結果中可以看出:a 是一個底層陣列有一個元素,容量為2的slice;append 1個元素後,沒有超出容量,產生了一個新的slice b,a 和 b 底層陣列首元素相同地址,說明a,b共用底層陣列;append 2個元素,超出了容量,產生一個新的slice c,c的底層陣列地址變了,容量也翻倍了。
那麼得出結論,append操作的執行過程:
1、如果新增資料後沒有超過原始容量,新的slice物件 和原始slice共用底層陣列,len 資料會變化,cap資料不變;
2、新增資料後超過了容量那就會擴容,重新分配一個新的底層陣列,然後拷貝底層陣列資料過去,那麼append後產生的新slice物件和原始的slice就沒有任何關係了。
擴容機制
看彙編程式碼可以知道,擴容呼叫的是底層函式 runtime.growslice
。
這個函式是這樣定義的:
func growslice(et *_type, old slice, cap int) slice {}
這個函式傳入三個引數:slice的原始型別,原始slice,期望的最小容量;返回一個新的slice,新slice 至少是擁有期望的最小容量,元素從原slice copy過來。
擴容規則主要是下面這段程式碼:
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
}
}
}
擴容規則就是兩點:
- 如果期望的最小容量大於原始的兩倍容量時,那麼新的容量就是等於期望的最小容量;
- 不滿足第一種情況,那麼判斷原slice的底層陣列元素長度是不是小於1024。小於1024,新容量是原來的兩倍;大於等於1024 ,新容量是原來的1.25倍。
上面是擴容的基本規則判斷,實際上擴容還要考慮到記憶體對齊情況:
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}
記憶體對齊之後,擴容的倍數就會 >= 2 或則 1.25 了。
為什麼要記憶體對齊?
1.平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
2.效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。
五、函式呼叫中實參和形參的相互影響
1、slice值傳遞,在呼叫函式中直接操作底層陣列
來看下面一段程式碼:
package main
import (
"fmt"
)
func OpSlice(b []int32) {
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 5, cap: 5, data:[1 2 3 4 5]
fmt.Printf("b第一個元素地址:%p\n", &b[0])
//b第一個元素地址:0xc420016120
b[0] = 100
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 5, cap: 5, data:[100 2 3 4 5]
fmt.Printf("b第一個元素地址:%p\n", &b[0])
//b第一個元素地址:0xc420016120
}
func main() {
a := []int32{1, 2, 3, 4, 5}
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 5, cap: 5, data:[1 2 3 4 5]
fmt.Printf("a第一個元素地址:%p\n", &a[0])
//a第一個元素地址:0xc420016120
OpSlice(a)
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 5, cap: 5, data:[100 2 3 4 5]
fmt.Printf("a第一個元素地址:%p\n", &a[0])
//a第一個元素地址:0xc420016120
}
從這段程式碼的列印中可以看到:
main函式中的slice a 是實參,值傳遞給呼叫函式時,要臨時拷貝一份給b,所以a,b 的地址是不一樣的,slice b 結構體中的三個元素都是a中的拷貝,但是元素array是指標,指標的拷貝還是指標,他們指向同一塊底層陣列,所以a,b底層陣列的第一個元素地址是一樣的。a,b共用同一塊底層陣列,在呼叫函式中,直接改變b的第一個元素內容,函式返回後a的第一個元素也變了,相當於改變了實參。
2、slice 指標傳遞
slice 指標傳遞就沒什麼說的了,在被呼叫函式中相當於操作的是實參中同一個slice,所有修改都會反映到實參。
3、slice 切片傳遞
不擴容的情況,來看下面一段程式碼:
package main
import (
"fmt"
)
func OpSlice(b []int32) {
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 3, cap: 9, data:[1 2 3]
fmt.Printf("b第一個元素地址:%p\n", &b[0])
//b第一個元素地址:0xc42007a064
b = append(b, 100)
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(b), cap(b), b)
//len: 4, cap: 9, data:[1 2 3 100]
fmt.Printf("b第一個元素地址:%p\n", &b[0])
//b第一個元素地址:0xc42007a064
}
func main() {
a := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("a第2個元素地址:%p\n", &a[1])
//a第2個元素地址:0xc42007a064
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 10, cap: 10, data:[0 1 2 3 4 5 6 7 8 9]
fmt.Printf("a第一個元素地址:%p\n", &a[0])
//a第一個元素地址:0xc42007a060
OpSlice(a[1:4])
fmt.Printf("len: %d, cap: %d, data:%+v \n", len(a), cap(a), a)
//len: 10, cap: 10, data:[0 1 2 3 100 5 6 7 8 9]
fmt.Printf("a第一個元素地址:%p\n", &a[0])
//a第一個元素地址:0xc42007a060
}
前面已經講過,切片和原slice是共用底層陣列的。不擴容情況下,對切片產生的新的slice append 操作,新增加的元素會新增到底層陣列尾部,會覆蓋原有的值,反映到原slice中去;
總結
無論是slice的什麼操作:拷貝,append,reSlice 等等都會產生新的slice,但是他們是共用底層陣列的,不擴容情況,他們增刪改元素都會影響到原來的slice底層陣列;擴容情況下,產生的是一個“真正的”新的slice物件,和原來的完全獨立開了,底層陣列完全不會影響。
參考資料
- 深度解密Go語言之Slice.
- Go語言學習筆記.