數(shù)據(jù)類型的本質(zhì):固定內(nèi)存大小的別名。
數(shù)據(jù)類型的作用:編譯器預(yù)算對象或變量分配內(nèi)存空間的大小。
數(shù)組 array
數(shù)組是同一種數(shù)據(jù)類型的固定長度的序列,指向一段連續(xù)的內(nèi)存空間。
聲明
arr := [6]int{1,2,3,4,5,6}
特性
- 因為數(shù)組的內(nèi)存分布是連續(xù)的,所以數(shù)組訪問任一元素的效率很高,O(1)。
- 元素類型和數(shù)組長度都是數(shù)組類型的一部分,不同長度的數(shù)組是不同的類型。
- 數(shù)組是值類型,改變其副本的值不會改變原數(shù)組的值。
數(shù)組指針和指針數(shù)組
數(shù)組指針
即聲明一個指針變量,指向一個數(shù)組:
arr := [6]int{5:9} //第6個元素為9
var ptr *[6]int = &arr
需要注意的是,指針變量ptr的類型是*[6]int,也就是說它只能指向包含6個元素的整型數(shù)組,否則編譯報錯。
指針數(shù)組
即數(shù)組的元素類型是指針。
x,y := 1,2
var arrPtr = [5]*int{1:&x,3:&y}
函數(shù)間傳遞數(shù)組
如果實參是一個非常大的數(shù)組,因函數(shù)參數(shù)只有值傳遞,即需要重新拷貝變量,拷貝導(dǎo)致的內(nèi)存和性能開銷較大??蓪?shù)組指針作為參數(shù)傳遞,拷貝開銷小,但也需要考慮到函數(shù)內(nèi)數(shù)組指針修改會影響原數(shù)組。
切片 slice
切片與數(shù)組類似,存放相同數(shù)據(jù)類型的元素,不同的是切片基于底層數(shù)組,可按需擴縮容。切片是底層數(shù)組的一個視圖,也可以說切片是對數(shù)組的抽象。
切片的內(nèi)部實現(xiàn)中有三個變量,指針 ptr,長度 len 和容量 cap。
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 指針,數(shù)據(jù)存儲在底層數(shù)組中,而指針指向可以通過切片訪問到的第一個元素。
len int // 長度,我們只能訪問切片長度范圍內(nèi)的元素。
cap int // 容量,表示可以擴展的最大大小。容量必須大于等于長度。
}
應(yīng)注意底層數(shù)組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進行操作是有可能影響到其他 slice 的。
聲明與初始化
make 函數(shù)創(chuàng)建
s1 := make([]int,3,5) // 創(chuàng)建一個可用的空切片,長度為3,容量為5,容量也可以不傳
fmt.Println(s1) // [0 0 0]
字面量創(chuàng)建
s2 := []int{1,2,3,4,5} // 創(chuàng)建長度和容量都是5的整型切片
截取已有的數(shù)組或者切片創(chuàng)建
a := []int{1,2,3,4,5}
t := a[3:4]
截取得到的切片,和原切片或原數(shù)組共享底層數(shù)組,但是兩者能訪問到底層數(shù)組的范圍是不同的。
截取獲得的新切片的長度和容量計算
對容量為 k 的切片執(zhí)行[i,j]操作之后,獲得的新切片的長度和容量是j-i和k-i。
對容量為 k 的切片執(zhí)行[i,j,l]操作之后,獲得的新切片的長度和容量是j-i和l-i,其中第三個變量 l 用于限定新切片的容量,j 和 l 必須在原數(shù)組或者原 slice 的容量范圍內(nèi)(小于等于 k)。
新切片與原數(shù)組或者切片的關(guān)系
截取得到的新切片和原數(shù)組或原切片是基于同一個底層數(shù)組的,所以當(dāng)修改的時候,底層數(shù)組的值就會被改變,原切片的值也隨之改變了。
可以把截取得到的新切片看做是原數(shù)組或原切片的一個視圖。
但如果因為執(zhí)行 append 操作使得新 slice 底層數(shù)組擴容,移動到了新的位置,兩者就不會相互影響了。所以,問題的關(guān)鍵在于兩者是否會共用底層數(shù)組。
nil 切片和空切片
nil 切片
var s []int // 聲明了一個 nil 切片
fmt.Println(s == nil) // 輸出 true
fmt.Println(len(s), cap(s)) // 輸出:0 0
s = append(s, 1) // 使用 append 函數(shù)可以為 nil 切片增加元素
切片的零值是 nil。因為切片是底層數(shù)組的引用,nil 切片指向底層數(shù)組的指針為 nil,即不指向任何底層數(shù)組。
空切片
s := make([]int, 0) // 1、使用 make 創(chuàng)建空的整型切片
s := []int{} // 2、使用切片字面量創(chuàng)建空的整型切片
fmt.Println(s == nil) // 輸出 false
fmt.Println(s) // 輸出:[]
fmt.Println(len(s), cap(s)) // 輸出:0 0
與 nil 切片一樣,空切片的長度和容量也都是 0,說明切片底層的數(shù)組大小為 0,是一個空數(shù)組(沒有分配任何的存儲空間)。
不管是使用 nil 切片還是空切片,對其調(diào)用內(nèi)置函數(shù) append、len 和 cap 的效果都是一樣的。官方建議盡量使用 nil 切片。
nil slice 可以直接 append ,因為 append 最終都是調(diào)用 mallocgc 來向 Go 的內(nèi)存管理器申請到一塊內(nèi)存,然后再賦給原來的 nil slice 或 empty slice。
增長/擴容
使用內(nèi)建函數(shù) append 能夠幫我們處理切片增長的一些細節(jié)。
- append 可往切片追加一個或多個值,然后返回一個新的切片。應(yīng)注意 Go 編譯器不允許調(diào)用了 append 函數(shù)后不使用返回值。
- append 函數(shù)會使新的切片長度增加。
- append 函數(shù)實際上是往底層數(shù)組添加元素。
- 新切片容量是否增長取決于原切片剩余容量和需要追加的元素數(shù)目。當(dāng)剩余容量不足時,append 函數(shù)會創(chuàng)建一個新的數(shù)組并將原數(shù)組元素拷貝到新數(shù)組中,再追加新的值。append 函數(shù)會智能地增加底層數(shù)組的容量,目前的算法是:當(dāng)數(shù)組容量小于等于1024時,會成倍地增加;當(dāng)超過1024,增長因子變?yōu)?.25,也就是說每次會增加25%的容量(這個說法是錯誤的,還有內(nèi)存對齊的操作)。
要注意的是,通過截取創(chuàng)建的切片,如果切片剩余容量能存下 append 追加的元素,切片長度增長而不擴容,追加的元素會改變原切片或數(shù)組的值;如果不能存下追加的元素,切片長度增長并進行擴容,擴容操作為 append 函數(shù)創(chuàng)建一個新的底層數(shù)組,將原數(shù)組的值復(fù)制到新數(shù)組里,再追加新的值,因為新切片與原切片或數(shù)組的底層數(shù)組不再相同,追加的元素不會改變原切片或數(shù)組的值,且后續(xù)兩個切片不再相互影響。關(guān)鍵在于兩者是否會共用底層數(shù)組。
一般我們在創(chuàng)建新切片的時候,最好要讓新切片的長度和容量一樣,這樣我們在追加操作的時候就會生成新的底層數(shù)組,和原有數(shù)組分離,就不會因為共用底層數(shù)組而引起奇怪問題。
copy 函數(shù)
Go 提供了內(nèi)置函數(shù) copy,可以將一個切片復(fù)制到另一個切片。
func copy(dst, src []Type) int // 函數(shù)返回兩者長度的最小值
如果 dst 切片為 nil 切片,copy 之后,dst 切片仍為 nil。而 nil 切片 append 非空切片后,變?yōu)榉强涨衅?/p>
copy 只會復(fù)制,不會追加。
函數(shù)間傳遞切片
切片在函數(shù)間以值的方式傳遞。當(dāng)直接用切片作為函數(shù)參數(shù)時,可以改變切片的元素,不能改變切片本身;想要改變切片本身,可以將改變后的切片返回,函數(shù)調(diào)用者接收改變后的切片或者將切片指針作為函數(shù)參數(shù)。
由于切片的尺寸很小(在 64 位架構(gòu)的機器上,一個切片需要 24 字節(jié)的內(nèi)存:指針字段、長度和容量字段各需要 8 字節(jié)),在函數(shù)間復(fù)制和傳遞切片成本很低。
刪除切片中的元素
Go 沒有提供刪除切片元素的函數(shù),可以通過截取和 append 函數(shù)實現(xiàn)。
s := []int{1, 2, 3, 4, 5, 6}
s = append(s[:2], s[3:]...) // 刪除索引為2的元素
切片垃圾回收
巨型 slice 產(chǎn)生的垃圾回收問題