Go 語言入門 3-動態數組(slice)的特性及實現原理
go 語言中的動態數組(slice),是基於數組實現的,可以相比數組而言更加的靈活。其他語言的 slice 通常僅是一個 API, 但是 go 語言的 slice 不僅僅是一種操作, 也是一種數據結構。
我們先看一下 slice 的數據結構:
type slice struct {
array unsafe.Pointer // 數組指針
len int // 切片長度
cap int // 數組容量
}
源碼鏈接:
//github.com/golang/go/blob/c379c3d58d5482f4c8fe97466a99ce70e630ad44/src/runtime/slice.go#L15
非常簡單的結構組成: 數組指針, 長度, 容量。
slice 的操作:
初始化有 4 種方式:
// 1. 變數聲明
var slice []int
// 2. 字面量
slice := []int{} // 空的切片
slice2 := []int{1, 2} // 長度為 2 的切片
// 3. 通過 make 創建 slice
// 創建一個存儲 int 類型的
// len = 10, cap = 20
slice := make([]int, 10, 20)
// 4. 通過數組切片, 此時 slice 會與數組共用底層記憶體
// 獲取 array[3][4] 的數據, 且共用 array 後續的存儲空間.
// 所以 slice 的 len = 2, cap = 10 - 3 = 7
array := [10]int
slice := array[3:5]
擴容:
當 len > cap 時, slice 會觸發擴容, 提高 cap,也就是實際數組容量。
我們來看一下 slice 擴容操作:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
// 小的 slice 擴容時 cap 直接翻倍
newcap = doublecap
} else {
// 檢測溢出和無限循環
for 0 < newcap && newcap < cap {
// 大的 slice 擴容時擴充 1.25 倍
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
源碼地址:
//github.com/golang/go/blob/c379c3d58d5482f4c8fe97466a99ce70e630ad44/src/runtime/slice.go#L188
針對 cap < 256 的 slice,擴容時 newcap = cap * 2
針對 cap >= 256 的 slice, 擴容時 newcap = (cap + 3 * 256) / 4
注意:網上很多文章會寫閾值為 1024, 且針對較大的 slice, 採取 1.25 倍擴容策略, 但這種演算法是低版本 go 中的計算方式。
那麼我們來思考一下, 為什麼要這麼實現呢?
因為針對較小的 slice,每次擴容增加較充足的容量可以減少記憶體重分配的次數以及數據遷移的成本。
針對較大的 slice, 每次擴容增加相對較少的容量可以避免記憶體資源浪費。
添加元素
slice := make([]int, 0)
slice = append(slice, 1) // 添加 1 個元素
slice = append(slice, 2, 3, 4) // 添加多個元素
slice = append(slice, []int{5, 6}...) // 添加一個切片
如果 cap >= len + 1,則直接追加元素到 slice 中, len++,返回 slice。
如果 cap < len + 1, 則擴容 slice 得到新的 slice, 然後追加元素到新的 slice, 新的 slice.len++, 返回新的 slice。
我們可以發現 slice 的操作相比其他數據結構要更加容易理解, 但在使用的時候一定要注意由於與底層數組是通過指針引用導致的共享記憶體問題。
關於 go 語言中 slice 相關的基礎知識就介紹這麼多了~
如果您在實際開發過程中遇到過 Python、Golang、資料庫、爬蟲、分散式、消息隊列等方面的難題, 也歡迎在公眾號或評論區留言, 我們一起探討解決方案
如果本篇內容能夠幫助到您, 希望您關注我的公眾號: 「python 學習愛好者」, 希望與您一起共同成長~