1. 程式人生 > 其它 >golang中陣列與切片的Q&A

golang中陣列與切片的Q&A

陣列與切片 的 Q&A

切片作為函式引數

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  1. slice其實是一個結構體,包含三個成員,len、map、array,分別表示長度、容量和底層陣列的地址
  2. 當slice作為函式引數時,就是一個普通的結構體,其實很好理解,若直接傳slice,在呼叫者看來,實參slice並不會
    被函式中的操作所改變,若傳的是slice的指標,在呼叫者看來,是會被改變原slice的
  3. 值得注意的是,不管傳遞的是slice還是slice指標,如果改變了slice底層陣列的資料,會反映到實參slice的底層資料
    為什麼能改變底層陣列的資料?
    很好理解,底層陣列資料在結構體裡是一個指標unsafe.Pointer型別,儘管slice傳遞是複製,但是它們指向底層陣列的指標是一樣的
  4. 通過slice的array欄位就可以拿到陣列的地址,在程式碼裡直接通過s[i] = 10這種操作改變slice底層陣列元素的值
  5. 另外值得注意的是,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  // 修改的是底層陣列的值
	}
}
  1. 果真改變了原始slice的底層資料,這裡傳遞的是一個slice副本,在f函式中,s只是main函式中s的一個拷貝,在f函式內部對s的作用
    並不會改變外層main函式中的s
  2. 要想真的改變外層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)

}
  1. 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]

切片的容量是怎樣增長的?

  1. 一般都是向slice追加了元素,才會引起擴容,追加元素呼叫的是append函式,先來看看append函式的原型
    func append(slice []Type, elems ...Type) []Type
  2. 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
  1. 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不會有影響
  1. 再看一個例子:
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"了

陣列和切片有什麼異同

  1. slice的底層資料是陣列,slice是對底層陣列的封裝,它描述一個數組的片段,兩者都可以通過下標訪問單個元素
  2. 陣列是定長的,長度定義好之後,不能在更改,在go中,陣列是不常見的,因為它的長度是型別的一部分,[3]int和[4]int是不同的型別
  3. 而切片非常的靈活,他可以動態擴容,而且它的型別和長度無關
  4. 陣列就是一片連續的記憶體,slice實際上是一個結構體,包含三個欄位array陣列指標,len長度,cap容量
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指標
    len   int // 長度 
    cap   int // 容量
} 
  • 一定要知道,陣列是一塊連續的記憶體
  1. 注意, 底層陣列可以被多個slice同時指向的,因此對一個slice的元素進行操作,有可能影響到其它slice
    【引申1】[3]int和[4]int是同一個型別嗎
    答:不是,因為陣列的長度也是型別的一部分,這一點跟切片不太一樣
  2. 對切片進行[:],和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]
}