golang數組和切片深入分析

一、數組

1.1 數組賦值給數組

Go數組是值類型,因此賦值操作和函數傳參數會複製整個數組的數據,例:

func main() {
	a := [3]int{1, 2, 3}
	b := a
	fmt.Printf("a addr: %p, a[0] addr: %p\n", &a, &(a[0]))
	fmt.Printf("b addr: %p, b[0] addr: %p\n", &b, &(b[0]))
	test(a)
}

func test(arr [3]int) {
	fmt.Printf("arr addr: %p, arr[0] addr: %p\n", &arr, &(arr[0]))
}

結果:

a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0
b addr: 0xc04204a100, b[0] addr: 0xc04204a100

可以看到,b的地址和a的地址不同,同時可以看到,數組的地址即爲數組第一個元素的地址。

1.2 數組賦值給數組指針

上面已經看到數組直接賦值是值傳遞,可以考慮用指針來實現傳地址,例:

func main() {
	a := [3]int{1, 2, 3}
	b := &a
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, value: %p, b[0] addr: %p, value: %v\n", &b, b, &(b[0]), b)
}

結果:

a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc04206a018, value: 0xc04204a0e0, b[0] addr: 0xc04204a0e0, value: &[1 2 3]

可以看到,指針的地址和數組的地址不一樣,指針的值是數組的地址,即這裏指針b是一個指向數組a地址的變量。這樣無論是直接賦值給指針,還是在函數中用指針來傳遞,都可以達到不復制數組,並且修改原數組值的目的。

二、切片

切片運行時實際結構爲SliceHeader ,其結構定義爲:

    type SliceHeader struct {
    	Data uintptr
    	Len  int
    	Cap  int
    }

三個成員即切片指向的數據源數組地址,切片長度和切片容量。當切片之間傳遞時,實際上是SliceHeader 之間的值傳遞,由於傳遞之後,Data指向的數組地址是同一個,所以修改操作會同步。下面從幾個實際例子來探究不同情況下的運行結果。

1.2 從數組得到切片

1、切片得到數組時,如果切片沒有進行擴容,則指向的數據源還是此數組,任何對數組或切片的值修改操作,另一個的值也隨之改變

切片未擴容

Go 切片是可以從數組得到的,以下代碼,從切片得到數組:

func main() {
	a := [3]int{1, 2, 3}
	b := a[:]
	fmt.Printf("a addr: %p, a[0] addr: %p\n", &a, &(a[0]))
	fmt.Printf("b addr: %p, b[0] addr: %p\n", &b, &(b[0]))
}

結果:

a addr: 0xc04200c4a0, a[0] addr: 0xc04200c4a0
b addr: 0xc0420023e0, b[0] addr: 0xc04200c4a0

可以看到,切片b指向了新地址,但是第一個元素的地址和數組a的一致。那這裏修改數組某個元素值切片的值會改變麼,或者修改切片的某個元素值數組的值會改變麼?下面繼續測試:

func main() {
	a := [3]int{1, 2, 3}
	b := a[:]
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b
	a[0] = 10
	b[1] = 20
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

結果:

a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [1 2 3]E
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [10 20 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [10 20 3]

可以看到,不管是修改數組的值,還是切片的值,另一個對應的值也改變了,這驗證了切片指向的數據源是數組a。

切片擴容情況

下面是從數組得到切片後,執行append操作:

func main() {
	a := [3]int{1, 2, 3}
	b := a[:]
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
	b = append(b, 4)
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
	
	a[0] = 10
	b[1] = 20
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

結果:

a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [1 2 3]
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc042068030, value: [1 2 3 4]
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [10 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc042068030, value: [1 20 3 4]

可以看到,擴容後切片b第一個元素的地址發生了變化,因此後續對數組值得修改和對切片值的修改,都不會影響到另一個的值。

切片之間賦值

Golang中切片是引用類型,直接賦值後,修改任意一個的某個元素值,另一個也會隨之改變。但是如果在賦值之後,切片進行了擴容操作,則會指向新的數據源,因此修改值操作不會影響原來的切片。如果不想影響另一個的結果,可以用copy函數來實現。例:

func main() {
	a := []int{1, 2, 3}
	b := a
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)  //b[0]地址和a[0]地址一樣
	fmt.Println("info a:", len(a), cap(a))
	fmt.Println("info b:", len(b), cap(b))

	b = append(b, 4)
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)  //b[0]地址變化了
	fmt.Println("info a:", len(a), cap(a))
	fmt.Println("info b:", len(b), cap(b))

	a[0] = 10
	b[1] = 20
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

結果:

a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc04204a0e0, value: [1 2 3]
info a: 3 3
info b: 3 3
a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 2 3 4]
info a: 3 3
info b: 4 6
a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [10 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 20 3 4]

切片append分析

當容量足夠時,直接在當前長度的下一個位置保存數據,即使還有其他切片也指向這一塊地址。例:

func main() {
	data := [5]int{1, 2, 3, 4, 5}
	a := data[0:1]
	b := data[0:4]
	fmt.Println("info a:", len(a), cap(a)) 
	fmt.Println("info b:", len(b), cap(b))  //切片a和b數據源都指向data,但是長度不同
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
	a = append(a, 10)  //向a添加數據,直接在數據第二個位置保存數據,這樣data結果也改變了
	fmt.Println("info a:", len(a), cap(a))
	fmt.Println("info b:", len(b), cap(b))
	fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
	fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

結果:

info a: 1 5
info b: 4 5
a addr: 0xc0420463a0, a[0] addr: 0xc042068030, value: [1]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 2 3 4]
info a: 2 5
info b: 4 5
a addr: 0xc0420463a0, a[0] addr: 0xc042068030, value: [1 10]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 10 3 4]

可以看到,由於多個切片指向同一塊數據源,任何修改操作都會使得其他變量結果改變。

總結

對於切片,只要清楚它指向的數據源是否有改變,即關注append前後時,容量是否有變化,就能判斷其運行結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章