切片定義
切片是基于數組實現的,它的底層是數組,可以理解為對 底層數組的抽象。切片底層結構并沒有使用加鎖等方式,不支持并發(fā)讀寫,所以并不是線程安全的
源碼包中src/runtime/slice.go 定義了slice的數據結構:
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice占用24個字節(jié)
- array: 指向底層數組的指針,占用8個字節(jié)
- len: 切片的長度,占用8個字節(jié)
- cap: 切片的容量,cap 總是大于等于 len 的,占用8個字節(jié)
slice有4種初始化方式
// 初始化方式1:直接聲明
var slice1 []int
// 初始化方式2:使用字面量
slice2 := []int{1, 2, 3, 4}
// 初始化方式3:使用make創(chuàng)建slice
slice3 := make([]int, 3, 5)
// 初始化方式4: 從切片或數組“截取”
slcie4 := arr[1:3]
切片和數組的區(qū)別:
- 數組長度不同
數組初始化必須指定長度,并且長度就是固定的
切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大
- 函數傳參不同
數組是值類型,將一個數組賦值給另一個數組時,傳遞的是一份深拷貝,函數傳參操作都會復制整個數組數據,會占用額外的內存,函數內對數組元素值的修改,不會修改原數組內容。
切片是引用類型,將一個切片賦值給另一個切片時,傳遞的是一份淺拷貝,函數傳參操作不會拷貝整個切片,只會復制len和cap,底層共用同一個數組,不會占用額外的內存,函數內對數組元素值的修改,會修改原數組內容。
- 計算數組長度方式不同
數組需要遍歷計算數組長度,時間復雜度為O(n)
切片底層包含len字段,可以通過len()計算切片長度,時間復雜度為O(1)
切片拷貝
- 深拷貝
拷貝的是數據本身,創(chuàng)造一個新對象,新創(chuàng)建的對象與原對象不共享內存,新創(chuàng)建的對象在內存中開辟一個新的內存地址,新對象值修改時不會影響原對象值, 深拷貝的方式有如下兩種
- copy(slice2, slice1)
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, 5, 5)
fmt.Printf("slice1: %v, %p\n", slice1, slice1)
copy(slice2, slice1)
fmt.Printf("slice2: %v, %p", slice2, slice2)
}
- 遍歷append賦值
func main() {
slice1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice1: %v, %p\n", slice1, slice1)
slice2 := make([]int, 5, 5)
for i, v := range slice1 {
slice2[i] = v
}
fmt.Printf("slice2: %v, %p", slice2, slice2)
上述兩種拷貝的方式,slice2 的地址與slice1 地址不同,所以是深拷貝
- 淺拷貝
拷貝的是數據地址,只復制指向的對象的指針,此時新對象和老對象指向的內存地址是一樣的,新對象值修改時老對象也會變化
引用類型的變量,默認賦值操作就是淺拷貝,例如如下的
slice2 := slice1
func main() {
slice1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice1: %v, %p\n", slice1, slice1)
slice2 := slice1
fmt.Printf("slice2: %v, %p", slice2, slice2)
}
//結果:slice1 和slice2 的底層地址相同,即指向同一個底層數組
slice1: [1 2 3 4 5], 0xc00001a120
slice2: [1 2 3 4 5], 0xc00001a120
擴容機制
擴容會發(fā)生在slice append的時候,當slice的cap不足以容納新元素,就會進行擴容,擴容規(guī)則如下
如果新申請容量比兩倍原有容量大,那么擴容后容量大小 為 新申請容量
如果原有 slice 長度小于 1024, 那么每次就擴容為原來的 2 倍
如果原 slice 長度大于等于 1024, 那么每次擴容就擴為原來的 1.25 倍
注意:切片發(fā)生擴容后,底層的數組指針會指向新的擴容后的數組