概述
在使用 Go 開發的時候,陣列和切片經常被使用到,這篇文章來簡單聊聊吧。
陣列array
在 Go 中,有兩種方式可以初始化陣列
func main() {
userId := [3]int{1, 2, 3}
userName := [...]string{"wqq", "curry", "joke"}
}
一種是顯式的定義陣列的大小,另一種通過 […] 宣告陣列,Go 會在編譯期間推匯出陣列的大小。
既然使用了陣列,少不了遍歷,在 Go 中遍歷陣列一般也就兩種方式。
func main() {
userIds := [3]int{1, 2, 3}
names := [...]string{"wqq", "curry", "joke"}
for i := 0; i < len(names); i++ {
fmt.Printf("user id is:%v,user name is:%v\n", userIds[i], names[i])
}
for index, item := range names {
fmt.Printf("user id is:%v,user name is:%v\n", userIds[index], item)
}
}
第一種就是你所認知的 for 迴圈。第二種可以使用 for/range 表示式,該表示式返回兩個值,第一個值是索引,第二個值對應此索引的元素值。range 不單單能遍歷陣列,還能遍歷 slice、map、channel 等集合結構。當然這些不在這篇文章的討論範圍內。
切片slice
切片本質上是動態陣列,它的底層包含了對陣列的引用。切片的長度是動態的,可以隨意的對其進行 append 操作,在使用的過程中,如果容量不足,會自動進行擴容操作。我們可以從原始碼看看 slice 的結構。原始碼位於 src/runtime/slice.go ,更多底層知識可以自行檢視原始碼。
type slice struct {
array unsafe.Pointer // 底層陣列的指標位置
len int // 切片當前長度
cap int //容量,當容量不夠時,會觸發動態擴容的機制
}
同理,初始化 slice 的方式也是多樣的。
使用 make 關鍵字
和陣列一樣,使用字面量初始化
通過下標的方式獲取陣列或者切片的一部分,生成 slice
func main() {
// 字面量初始化
userIds1 := []int{1, 2, 3}
// make初始化slice的長度為5,容量為10
userIds2 := make([]int, 5,10)
// 通過下標的方式獲取陣列的一部分作為alice
userArray := [5]string{"curry", "wqq", "lisa", "tony", "james"}
// 獲取從索引下標0開始,到下標3(不包括3)
user := userArray[0:3]
fmt.Printf("userIds1:%v,userIds2:%v,userSlice:%v\n", userIds1, userIds2, user)
}
這裡就拿 make 初始化切片進行說明。
注:圖片來源 《Go程式設計專家》
這段初始化操作表示 slice 的長度是 5,容量是 10,array 欄位儲存的是引用陣列的指標位置。因為長度是 5,我們可以使用下標 0-4 來操作此 slice。同時容量是 10,所以後續向 slice 新增新資料暫時不需要重新分配新記憶體。
那陣列和切片有什麼關聯呢?
我們看看通過下標的方式獲取陣列資料,初始化切片的一種形式。
func main() {
userArray := [4]string{"curry", "wqq", "lisa", "tony"}
// 獲取從索引下標0開始,到下標3(不包括3)
userSlice := userArray[0:3]
userSlice[0] = "zhangsan"
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}
我們用陣列建立了 userSlice 的切片,此時 userSlice 將和 userArray 共用一部分記憶體。因此在修改 userSlice 索引 0 處的值時,操作的是同一塊陣列記憶體地址,從結果中可以看出生效了。
然後我們開始往 userSlice 切片新增元素。
func main() {
userArray := [4]string{"curry", "wqq", "lisa", "tony"}
// 獲取從索引下標0開始,到下標3(不包括3)
userSlice := userArray[0:3]
userSlice[0] = "zhangsan"
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
userSlice = append(userSlice, "test1")
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}
檢視輸出結果:
可以看到,再向 userSlice 增加一個元素後,列印結果,陣列和切片值一樣,操作之後 userSlice 的 len 是 4,陣列的長度也是 4。操作 append 後 userSlice 底層陣列和 userArray 指向的還是同一個記憶體地址,並不需要發生擴容。
這時候,userSlice 所引用的底層陣列已經滿了(底層陣列的長度是4),我們繼續向 userSlice 增加元素。
func main() {
userArray := [4]string{"curry", "wqq", "lisa", "tony"}
// 獲取從索引下標0開始,到下標3(不包括3)
userSlice := userArray[0:3]
userSlice[0] = "zhangsan"
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
userSlice = append(userSlice, "test1")
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
userSlice = append(userSlice, "test2")
fmt.Printf("userArray:%v,user:%v\n", userArray, userSlice)
}
檢視輸出結果:
可以看到,userArray 的元素未變,因為這時候 userSlice 切片的長度已經大於原指向的陣列的長度了, userSlice 發生了擴容。
我們可以做個實驗測試一下,我們修改陣列 userArray 範圍內的 userSlice 元素的值,檢視陣列的資料是否會跟著改變。
func main() {
userArray := [4]string{"curry", "wqq", "lisa", "tony"}
// 獲取從索引下標0開始,到下標3(不包括3)
userSlice := userArray[0:3]
userSlice[0] = "zhangsan"
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
userSlice = append(userSlice, "test1")
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
userSlice = append(userSlice, "test2")
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
// 改變索引0處的值
userSlice[0] = "only one"
fmt.Printf("userArray:%v,userSlice:%v\n", userArray, userSlice)
}
最後一行已經說明了一切。此時的 userSlice 發生了擴容,不再和 userArray 共用原陣列空間了。因此對 userSlice 的改動不會影響到 userArray。
關於擴容
前面提到在向切片新增新元素時如果此時切片的容量不足,會自動發生擴容。所謂擴容,也就是為當前切片生成新的一塊記憶體空間,然後根據一定規則,將原切片的元素全部拷貝到新的地址。擴容的規則在 src/runtime/slice.go 裡的 growslice 方法。
這裡擷取了此方法中關於擴容規則的程式碼。
如果期望的新容量 (cap) 大於當前容量的兩倍,那麼就直接使用期望的容量
如果當前切片的長度 (len) 小於 1024,那麼把當前容量翻倍
如果當前切片的長度(len) 大於等於 1024,那麼每次把當前容量增加 1/4,直到新容量值大於期望的的容量。
其實要寫下去還有很多東西,比如,sliceCopy、底層編譯邏輯……,有些東西我也沒看過,學習的最好方式還是自己動手然後輸出。
參考資料:
本作品採用《CC 協議》,轉載必須註明作者和本文連結