golang中陣列與切片的Q&A
陣列與切片 的 Q&A
切片作為函式引數
type slice struct {
array unsafe.Pointer
len int
cap int
}
- slice其實是一個結構體,包含三個成員,len、map、array,分別表示長度、容量和底層陣列的地址
- 當slice作為函式引數時,就是一個普通的結構體,其實很好理解,若直接傳slice,在呼叫者看來,實參slice並不會
被函式中的操作所改變,若傳的是slice的指標,在呼叫者看來,是會被改變原slice的 - 值得注意的是,不管傳遞的是slice還是slice指標,如果改變了slice底層陣列的資料,會反映到實參slice的底層資料
為什麼能改變底層陣列的資料?
很好理解,底層陣列資料在結構體裡是一個指標unsafe.Pointer型別,儘管slice傳遞是複製,但是它們指向底層陣列的指標是一樣的 - 通過slice的array欄位就可以拿到陣列的地址,在程式碼裡直接通過s[i] = 10這種操作改變slice底層陣列元素的值
- 另外值得注意的是,go語言的函式引數傳遞,只有值傳遞,沒有引用傳遞
func main() { s := []int{1, 1, 1} fmt.Println(s) f(s) fmt.Println(s) } func f(s []int) { // range會拷貝一份(copy方法),不會對原切片造成影響 //for _, v := range s{ // v++ //} for i, _ := range s{ s[i] += 1 // 修改的是底層陣列的值 } }
- 果真改變了原始slice的底層資料,這裡傳遞的是一個slice副本,在f函式中,s只是main函式中s的一個拷貝,在f函式內部對s的作用
並不會改變外層main函式中的s - 要想真的改變外層slice,只有將返回新的slice賦值到原始slice,或者向函式傳遞一個指向slice的指標,下面的例子
func myAppend(s []int) []int { // append方法導致了切片的擴容,底層陣列複製,所以不會影響到外部切片 s = append(s, 100) return s } func myAppendPtr(s *[]int) { // append方法導致了切片的擴容,底層陣列複製,但是擴容也好,複製也巴都是針對外部切片的,因為傳遞進來的是指標 *s = append(*s, 1000) } func main() { s := []int{1, 2, 3} newS := myAppend(s) fmt.Println(s) fmt.Println(newS) s = newS fmt.Println(s) myAppendPtr(&s) fmt.Println(s) }
- myAppend函式裡雖然改變了s,但它只是一個值傳遞,並不會影響最外層的s,因此第一行列印[1 2 3]
而newS是一個新的slice,是基於s得到的,因此它列印的是追加了100之後的結果,[1,2,3,100]
最後newS賦值給了s,s這是才真正變成了一個新的slice,在給myAppendPtr傳入一個s的指標,這回它真的改變了[1,2,3,100,1000]
切片的容量是怎樣增長的?
- 一般都是向slice追加了元素,才會引起擴容,追加元素呼叫的是append函式,先來看看append函式的原型
func append(slice []Type, elems ...Type) []Type
- append函式的引數長度可變,因此可以追加多個值到slice中,也可以追加一個切片...
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
func main() {
s1 := []int{1, 2, 3}
// 陣列的指標就是陣列中第一個元素的指標
// 切片的地址 切片的值 切片指向底層陣列的地址 == 切片指向底層陣列第一個元素的地址
fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))
// 切片擴容,底層陣列複製,切片的地址不變,但是底層陣列的地址改變了
s1 = append(s1, []int{4, 5}...)
fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))
}
輸出結果
0xc000004078:[1 2 3]:0xc000010180:0xc000010180
0xc000004078:[1 2 3 4 5]:0xc00000c360:0xc00000c360
- append()函式的返回值是一個新的slice, go編譯器不允許呼叫了append函式後不使用返回值
append(slice, elem1, elem2)
append(slice, anotherSlice...)
所以上面這種寫法是錯的,編譯器不能通過
4. 使用append可以向slice新增元素,實際上是向底層陣列新增元素,但是底層陣列的長度是固定的,如果索引
len-1所指向的元素已經是底層陣列的最後一個元素,就沒辦法添加了
5. 切片擴容的規則
1. 當原slice的容量小於1024的時候,新slice的容量是原slice容量的2倍
2. 當原slice的容量大於1024的時候,新slice的容量是原slice容量的1.25倍
6. 其實上面說法是有誤的,原始碼中func growslice(et *_type, old slice, cap int) slice {
原始碼上半部分確實如上所說進行擴容,但是後面會進行記憶體對齊,這個和記憶體分配策略相關,記憶體對齊後,
新slice的容量要大於等於就slice容量的2倍或1.25倍
7. 經典例子1
func main() {
s := []int{5} // s長度1,容量1
s = append(s, 7) // s長度2,容量2
s = append(s, 9) // s長度3, 容量4
x := append(s, 11) // x長度4,容量4,s長度3,容量4 此處沒有發生擴容
y := append(s, 12) // y長度4,容量4,s長度3,容量4 此處也沒有發生擴容
fmt.Println(len(s), cap(s)) // 3 4
fmt.Println(len(x), cap(x)) // 4 4
fmt.Println(len(y), cap(y)) // 4 4
fmt.Println(s, x, y) // [5 7 9] [5 7 9 12] [5 7 9 12]
}
上面案例程式碼 切片對應狀態
s := []int{5} s 只有一個元素,[5]
s = append(s, 7) s 擴容,容量變為2,[5, 7]
s = append(s, 9) s 擴容,容量變為4,[5, 7, 9]。注意,這時 s 長度是3,只有3個元素
x := append(s, 11) 由於 s 的底層陣列仍然有空間,因此並不會擴容。這樣,底層陣列就變成了 [5, 7, 9, 11]。注意,此時 s = [5, 7, 9],容量為4;x = [5, 7, 9, 11],容量為4。這裡 s 不變
y := append(s, 12) 這裡還是在 s 元素的尾部追加元素,由於 s 的長度為3,容量為4,所以直接在底層陣列索引為3的地方填上12。結果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的長度均為4,容量也均為4
8. 經典例子2
在看一個例子,跟上面的有一點不太一樣
func main() {
s := []int{5} // s長度1,容量1
s = append(s, 7) // s長度2,容量2
s = append(s, 9) // s長度3, 容量4
x := append(s, 11) // x長度4,容量4,s長度3,容量4
y := append(x, 12) // y長度5,容量8,s長度3,容量4
fmt.Println(len(s), cap(s)) // 3 4
fmt.Println(len(x), cap(x)) // 4 4
fmt.Println(len(y), cap(y)) // 5 8
fmt.Println(s, x, y) // [5 7 9] [5 7 9 11] [5 7 9 11 12]
}
- 總結:這裡需要注意的是,append函式返回的是全新的slice, 並且對傳入的slice不會有影響
- 再看一個例子:
func main() {
s := []int{11, 22}
s = append(s, 33, 44, 55)
fmt.Println(s, len(s), cap(s)) // [11 22 33 44 55] 5 6
}
看一段原始碼
// 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)
}
我們可以看到,如果求的的新的容量cap>2倍的舊容量,那麼newcap直接等於新求的容量cap
由於後面有記憶體對齊相關的原始碼,最終容量變成了6,:
參考文件: https://www.bookstack.cn/read/qcrao-Go-Questions/陣列和切片-切片的容量是怎樣增長的.md
10. 向一個nil的slice新增元素會發生什麼,為什麼?
其實nil slice或者empty slice都可以通過append函式實現底層陣列的擴容,最終都是呼叫mallocgc向go的記憶體管理器申請一塊記憶體,
然後賦值給原來的nil slice或emtpy slice,然後搖身一變,變成了真正的"slice"了
陣列和切片有什麼異同
- slice的底層資料是陣列,slice是對底層陣列的封裝,它描述一個數組的片段,兩者都可以通過下標訪問單個元素
- 陣列是定長的,長度定義好之後,不能在更改,在go中,陣列是不常見的,因為它的長度是型別的一部分,[3]int和[4]int是不同的型別
- 而切片非常的靈活,他可以動態擴容,而且它的型別和長度無關
- 陣列就是一片連續的記憶體,slice實際上是一個結構體,包含三個欄位array陣列指標,len長度,cap容量
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指標
len int // 長度
cap int // 容量
}
- 一定要知道,陣列是一塊連續的記憶體
- 注意, 底層陣列可以被多個slice同時指向的,因此對一個slice的元素進行操作,有可能影響到其它slice
【引申1】[3]int和[4]int是同一個型別嗎
答:不是,因為陣列的長度也是型別的一部分,這一點跟切片不太一樣 - 對切片進行[:],和append元素的經典案例:
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // s1=[2, 3, 4] len=3 cap=8
s2 := s1[2:6:7] // s2=[4, 5, 6, 7] len=4 cap=5
// 下面程式碼會改變底層陣列的值:8->100,因為s1和slice也引用的底層陣列,所以它倆都會受到影響
// 此時slice = [0, 1, 2, 3, 4, 5, 6, 7, 100, 9], 由於s1的長度為3,所以它雖然底層陣列收到了影響,但是它沒變化
s2 = append(s2, 100) // s2=[4, 5, 6, 7, 100] len=5 cap=5
// s2發生了擴容,底層陣列複製,增加的200元素不會對s1和slice的底層陣列產生影響
s2 = append(s2, 200) // s2=[4, 5, 6, 7, 100, 200] len=6 cap=10
s1[2] = 20 // s1=[2, 3, 20] len=3 cap=8, slice的4->20 slice=[0, 1, 2, 3, 20, 5, 6, 7, 100, 9]
fmt.Println(s1) // [2, 3, 20]
fmt.Println(s2) // [4, 5, 6, 7, 100, 200]
fmt.Println(slice) // [0, 1, 2, 3, 20, 5, 6, 7, 100, 9]
}