Go切片全決議
目錄結構:
陣列
切片
- 底層結構
- 創建
- 普通宣告
- make方式
- 截取
- 邊界問題
- 追加
- 拓展運算式
- 擴容機制
- 切片傳遞的坑
- 切片的拷貝
- 淺拷貝
- 深拷貝
陣列
var n [4]int
fmt.Println(n) //輸出:[0 0 0 0]
n[0] = 1
n[3] = 2
fmt.Println(len(n)) //輸出: 4
fmt.Println(cap(n)) //輸出:4
fmt.Println(n) //輸出:[1 0 0 2]
b := n
n[0] = 2
fmt.Println(b) //輸出: [1 0 0 2]
b[0] = 3
fmt.Println(n) //輸出: [2 0 0 2]
說明:
- 在
var n [4]int就已經完成了陣列的初始化,并且全部賦值為0,長度和容量都為4 - 把n賦值給b,相當于對n進行
copy操作,再把copy后的結果賦值給b,所以n和b是分別屬于兩個陣列,互不影響
切片
底層結構
type slice struct {
array unsafe.Pointer // 指標指向底層陣列
len int // 切片長度
cap int // 底層陣列容量
}
創建
宣告方式
默認值是nil,初始的長度和容量都為0
var s []int
fmt.Println(cap(s)) // 0
fmt.Println(len(s)) // 0
fmt.Println(s == nil) // true
make創建
make([]interface{}, len, cap)
通過make創建,默認值不為nil,且初始的長度和容量都可指定,不受自動擴容機制干擾,并且當初始的len引數不為0時,會像陣列那樣自動賦值
a := make([]int, 0, 10)
fmt.Println(len(a)) // 0
fmt.Println(cap(a)) // 10
fmt.Println(a == nil) // false
b := make([]int, 1000)
fmt.Println(len(b)) // 1000
fmt.Println(cap(b)) // 1000
c := make([]int, 5, 10)
fmt.Println(len(c)) // 5
fmt.Println(cap(c)) // 10
fmt.Println(c) // [0 0 0 0 0]
截取
切片可以基于陣列和切片來創建,截取的規則是左閉右開
n := [5]int{1, 2, 3, 4, 5}
n1 := n[1:] // 從n陣列中截取
fmt.Println(n1) // [2 3 4 5]
n2 := n1[1:] // 從n1切片中截取
fmt.Println(n2) // [3 4 5]
切片與原陣列或切片是共享底層空間的,接著上面例子,把n2的元素修改之后,會影響原切片和陣列:
n2[1] = 6 // 修改n2,會影響原切片和陣列
fmt.Println(n1) // [2 3 6 5]
fmt.Println(n2) // [3 6 5]
fmt.Println(n) // [1 2 3 6 5]
邊界問題
- 1、當n為陣列或字串運算式
n[low:high]中low和high的取值關系:
0 <= low <=high <= len(n)
- 2、當n為切片的時候,運算式
n[low:high]中high最大值變成了cap(n),low和high的取值關系:
0 <= low <=high <= cap(n)
不滿足以上條件會發送越界panic,
不同點,有邊界陣列是len(n),切片是cap(n)
追加
內置函式append()用于向切片中追加元素,
n := make([]int, 0)
n = append(n, 1) // 添加一個元素
n = append(n, 2, 3, 4) // 添加多個元素
n = append(n, []int{5, 6, 7}...) // 添加一個切片
fmt.Println(n) // [1 2 3 4 5 6 7]
當append操作的時候,切片容量如果不夠,會觸發擴容,接著上面的例子:
fmt.Println(cap(n)) // 容量等于8
n = append(n, []int{8, 9, 10}...)
fmt.Println(cap(n)) // 容量等于16,發生了擴容
當一開始容量是8,后面追加了切片[]int{8, 9, 10}之后,容量變成了16,
如果append超過切片的長度會重新生產一個全新的切片,不會覆寫原來的:
n2 := n[1:4:5] // 長度等于3,容量等于4
fmt.Printf("%p\n", n2) // 0xc0000ac068
n2 = append(n2, 5)
fmt.Printf("%p\n", n2) // 0xc0000ac068
n2 = append(n2, 6)
fmt.Printf("%p\n", n2) // 地址發生改變,0xc0000b8000
拓展運算式
簡單運算式生產的新切片與原陣列或切片會共享底層陣列,雖然避免了copy,但是會帶來一定的風險,下面這個例子當新的n1切片append添加元素的時候,覆寫了原來n的索引位置4的值,導致你的程式可能是非預期的,從而產生不良的后果
n := []int{1, 2, 3, 4, 5, 6}
n1 := n[1:4]
fmt.Println(n) // [1 2 3 4 5 6]
fmt.Println(n1) // [2 3 4]
n1 = append(n1, 100) // 把n的索引位置4的值從原來的5變成了100
fmt.Println(n) // [1 2 3 4 100 6]
fmt.Println(n1) // [2 3 4 100]
fmt.Println(len(n[1:4])) // 3
fmt.Println(cap(n[1:4])) // 5
關于容量
n[1:4]的長度是3好理解(4-1),容量為什么是5?
因為切片n[1:4]和切片n是共享底層空間,所以它的容量并不等于他的長度3,根據1等于索引1的位置(等于值2),從值2這個元素開始到末尾元素6,共5個,所以n[1:4]容量是5,
Go 1.2[3]中提供了一種可以限制新切片容量的運算式:
n[low:high:max]
max表示新生成切片的容量,新切片容量等于max-low,運算式中low、high、max關系:
0 <= low <= high <= max <= cap(n)
繼續剛才的例子,會用max的值來重新計算容量,而不是共享n的容量,但是n2和n還是共享同一個底層陣列
n2 := n[1:4:5]
fmt.Println(cap(n2)) // 4
fmt.Println(n2) // 輸出 [2 3 4]
n[3] = 111
fmt.Println(n2) // 輸出 [2 3 111]
擴容機制
關于Go切片的擴容機制,網上文章很多,很多結論是這樣的:
結論1:
- 1、當需要的容量超過原切片容量的兩倍時,會使用需要的容量作為新容量,
- 2、當原切片長度小于1024時,新切片的容量會直接翻倍,而當原切片的容量大于等于1024時,會反復地增加25%,直到新容量超過所需要的容量,
結論2:
- 在結論1的基礎上(切片的預估容量階段),提到了
記憶體對齊,容量計算完了后還要考慮到記憶體的高效利用,進行記憶體對齊,
例子
package main
func main() {
s := []int{1,2}
s = append(s, 3,4,5)
println(cap(s)) //輸出6
}
由于初始 s 的容量是2,現需要追加3個元素,所以通過 append 一定會觸發擴容,并呼叫 growslice 函式,此時他的入參 cap 大小為2+3=5,通過翻倍原有容量得到 doublecap = 2+2,doublecap 小于 cap 值,所以在第一階段計算出的期望容量值 newcap=5,在第二階段中,元素型別大小 int 和 sys.PtrSize 相等,通過 roundupsize 向上取整記憶體的大小到 capmem = 48 位元組,所以新切片的容量newcap 為 48 / 8 = 6 ,成功解釋!
在切片 append 操作時,如果底層陣列已無可容納追加元素的空間,則需擴容,擴容并不是在原有底層陣列的基礎上增加記憶體空間,而是新分配一塊記憶體空間作為切片的底層陣列,并將原有資料和追加資料拷貝至新的記憶體空間中,
在擴容的容量確定上,相對比較復雜,它與CPU位數、元素大小、是否包含指標、追加個數等都有關系,當我們看完擴容原始碼邏輯后,發現去糾結它的擴容確切值并沒什么必要,
在實際使用中,如果能夠確定切片的容量范圍,比較合適的做法是:切片初始化時就分配足夠的容量空間,在append追加操作時,就不用再考慮擴容帶來的性能損耗問題,
切片傳遞的坑
例子1
有以下例子
func modifySlice(innerSlice []string) {
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 輸出如下
[b b]
[b b]
在上面的例子中,切片內容都得到了修改,
例子2
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 輸出如下
[b b a]
[a a]
說明:
- 在modifySlice方法中,innerSlice是outerSlice的副本,但是共同參考相同的底層陣列,所以在例子1中,切片內容都得到了修改,
- innerSlice是一個len和cap都相同的切片,當append方法發生時,會進行擴容操作,擴容操作會使得產生一個新的切片,是在原有的陣列中進行深拷貝,并且擴大容量,
對代碼的細節進行列印再次看一下輸出結果
func modifySlice(innerSlice []string) {
fmt.Println("begin modify")
innerSlice = append(innerSlice, "a")
fmt.Printf("%p, %v\n", innerSlice, &innerSlice[0])
fmt.Println("innerSlice len:", len(innerSlice), "cap:", cap(innerSlice))
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
fmt.Println("end modify")
}
func main() {
outerSlice := []string{"a", "a"}
fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
fmt.Println("outerSlice len:", len(outerSlice), "cap:", cap(outerSlice))
modifySlice(outerSlice)
fmt.Println("outerSlice len:", len(outerSlice), "cap:", cap(outerSlice))
fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
fmt.Print(outerSlice)
}
//輸出
0xc0000464e0, 0xc0000464e0
outerSlice len: 2 cap: 2
begin modify
0xc000022240, 0xc000022240 //地址轉換
innerSlice len: 3 cap: 4 //容量改變
[b b a]
end modify
outerSlice len: 2 cap: 2
0xc0000464e0, 0xc0000464e0
[a a]
證明了我們的猜想,
例子3
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := make([]string, 0, 3)
outerSlice = append(outerSlice, "a", "a")
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
//輸出
[b b a]
[b b]
說明:
- 初始化切片的容量為3,所以在
innerSlice不會發生擴容操作,但是由于是值傳遞,innerSlice只是outerSlice的一個副本,當進行append操作的時候,也是對同一個陣列進行插入,同時改變innerSlice的長度,但是outerSlice的長度(len欄位)并沒有發生改變,所以列印出來的還是[b b]
補充一下列印的細節并稍微做點處理
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
for { //不斷列印OuterSlice的記憶體地址以及值
time.Sleep(time.Second / 10)
fmt.Printf("%p\n", innerSlice)
fmt.Println(innerSlice)
}
}
func main() {
outerSlice := make([]string, 0, 3) //初始化容量為3長度為0的切片
outerSlice = append(outerSlice, "a", "a")
fmt.Printf("outerSlice %p\n", outerSlice) //列印innerSlice初始的記憶體地址
go modifySlice(outerSlice) //執行modifySlice
time.Sleep(time.Second / 5) //等待modifySlice結束
fmt.Println(outerSlice) //再次列印innerSlice的值
fmt.Println("outerSlice", len(outerSlice), cap(outerSlice)) //列印innerSlice的長度和容量
outerSlice = append(outerSlice, "b")
fmt.Println(outerSlice) ////再次列印innerSlice的值
fmt.Printf("outerSlice %p\n", outerSlice) //再次列印innerSlice的記憶體地址
time.Sleep(time.Second) //等待modify方法的輸出
}
//輸出
outerSlice 0xc0000c4c60 //outerSlice的初始的記憶體地址
0xc0000c4c60 //innerSlice的記憶體地址
[b b a] //modify后的值
[b b]
outerSlice 2 3
[b b b]
outerSlice 0xc0000c4c60 //outerSlice的記憶體地址沒有發生改變
0xc0000c4c60 //innerSlice的記憶體地址的值沒有發生改變
[b b b] //innerSlice的值被覆寫了
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
由此可以說明,當append()執行的時候,沒有進行擴容的話還是共享同一個陣列,但因為是值傳遞,innerSlice是一個副本,改變的是副本的len,outerSlice的len實際并沒有變化,所以輸出的值會比innerSlice少
切片的拷貝
淺拷貝
通過=運算子拷貝切片,這是淺拷貝,
func main() {
a := []int{1, 2, 3}
b := a
fmt.Println(unsafe.Pointer(&a)) // 0xc00000c030
fmt.Println(a, &a[0]) // [100 2 3] 0xc00001a078
fmt.Println(unsafe.Pointer(&b)) // 0xc00000c048
fmt.Println(b, &b[0]) // [100 2 3] 0xc00001a078
}
通過[:]方式復制切片,同樣是淺拷貝,
func main() {
a := []int{1, 2, 3}
b := a[:]
fmt.Println(unsafe.Pointer(&a)) *// 0xc0000a4018*
fmt.Println(a, &a[0]) *// [1 2 3] 0xc0000b4000*
fmt.Println(unsafe.Pointer(&b)) *// 0xc0000a4030*
fmt.Println(b, &b[0]) *// [1 2 3] 0xc0000b4000*
}
深拷貝
深拷貝,需要用到copy()內置函式
func copy(dst, src []Type) int
其回傳值代表切片中被拷貝的元素個數
func main() {
a := []int{1, 2, 3}
b := make([]int, len(a), len(a))
copy(b, a)
fmt.Println(unsafe.Pointer(&a)) *// 0xc00000c030*
fmt.Println(a, &a[0]) *// [1 2 3] 0xc00001a078*
fmt.Println(unsafe.Pointer(&b)) *// 0xc00000c048*
fmt.Println(b, &b[0]) *// [1 2 3] 0xc00001a090*
}
copy 的元素數量與原始切片和目標切片的大小、容量有關系,并且只是往原有的切片進行資料替換,不會產生新的切片
func main() {
a := []int{1, 2, 3}
b := []int{-1, -2, -3, -4}
c := []int{-1, -2}
fmt.Println(unsafe.Pointer(&b)) //0xc0000040f0
copy(b, a)
fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
fmt.Println(a, &a[0]) // [1 2 3] 0xc0000145e8
fmt.Println(unsafe.Pointer(&b)) // 0xc0000040f0
fmt.Println(b, &b[0]) // [1 2 3 -4] 0xc0000101e0
fmt.Println(unsafe.Pointer(&c)) //0xc000004108
copy(c, a)
fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
fmt.Println(a, &a[0]) // [1 2 3] 0xc0000145e8
fmt.Println(unsafe.Pointer(&c)) // 0xc000004108
fmt.Println(c, &c[0]) // [1 2] 0xc0000129a0
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/431402.html
標籤:Go
上一篇:容器化Linux環境中的C :為什么嘗試分配大向量會導致SIGABRT或永無止境的回圈而不是bad_alloc?
