由於文章由markdown的方式編寫, 部分鏈接與圖片顯示可能存在問題, 大家可以移步到github源查看, 文章摘錄: https://halfrost.com/go_slice/
ps. 測試環境:
Linux * 4.15.0-66-generic #75-Ubuntu SMP Tue Oct 1 05:24:09 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
ubuntu18.04
go version: 1.12.4
cpu: Intel(R) Core(TM) i5-8500 CPU @ 3.00GHz
memory: 16GB
-
數組爲值類型, 切片爲引用類型
-
切片本身並不是動態數組或者數組指針。它內部實現的數據結構通過指針引用底層數組,設定相關屬性將數據讀寫操作限定在指定的區域內。切片本身是一個只讀對象,其工作機制類似數組指針的一種封裝。
-
切片賦值會在堆上開闢空間, 證據:
package main
import "testing"
func array() [1024]int {
var x [1024]int
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func slice() []int {
x := make([]int, 1024)
for i := 0; i < len(x); i++ {
x[i] = i
}
return x
}
func BenchmarkArray(b *testing.B) {
for i := 0; i < b.N; i++ {
array()
}
}
func BenchmarkSlice(b *testing.B) {
for i := 0; i < b.N; i++ {
slice()
}
}
測試命令, 禁用內聯和優化
go test -bench . -benchmem -gcflags "-N -l"
測試結果
BenchmarkArray-6 1000000 2277 ns/op 0 B/op 0 allocs/op
BenchmarkSlice-6 500000 3191 ns/op 8192 B/op 1 allocs/op
結論
這樣對比看來,並非所有時候都適合用切片代替數組,因爲切片底層數組可能會在堆上分配內存,而且小數組在棧上拷貝的消耗也未必比 make 消耗大
切片結構的類型
type slice struct {
array unsafe.Pointer
len int
cap int
}
手動創建一個slice結構, 從內存中得到切片的方法:
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
fmt.Println(&s[0])
fmt.Println(ptr)
var s1 = struct {
addr unsafe.Pointer
len int
cap int
}{ptr, 200, 200}
s2 := *(*[]byte)(unsafe.Pointer(&s1))
fmt.Println(s2)
也可以更直接的:
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&s)))
sliceHeader.Cap = 200
sliceHeader.Len = 200
sliceHeader.Data = uintptr(ptr)
nil切片
nil 切片被用在很多標準庫和內置函數中,描述一個不存在的切片的時候,就需要用到 nil 切片。比如函數在發生異常的時候,返回的切片就是 nil 切片。nil 切片的指針指向 nil。
空切片
空切片和 nil 切片的區別在於,空切片指向的地址不是nil,指向的是一個內存地址,但是它沒有分配任何內存空間,即底層元素包含0個元素。
最後需要說明的一點是。不管是使用 nil 切片還是空切片,對其調用內置函數 append,len 和 cap 的效果都是一樣的。
slice擴容
Go 中切片擴容的策略是這樣的:
-
首先判斷,如果新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)
-
否則判斷,如果舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap)
-
否則判斷,如果舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始循環增加原來的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)
-
如果最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)
注意:擴容擴大的容量都是針對原來的容量而言的,而不是針對原來數組的長度而言的
Range的用法
代碼測試:
func main() {
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
}
}
測試結果:
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338