切片是Go的以中基础数据类型,它可以在程序运行中动态的扩大自己的容量,使用起来很方便。
一,切片和数组的区别
- 数组是值类型,赋值传递的过程中是值传递;切片是引用类型,赋值传递是引用传递。
- 数组声明的时候是需要一个定值长度的,切片不需要。
- 数组的长度是静态的,在声明是就确定了,切片是动态的变化的。
二,切片的数据结构
切片是一个引用类型,输出切片的得到一个指针,这并不能说明切片就是一个指针。可以将切片理解为一个对数组指针的封装。一下是切片的结构体:
type slice struct {
array unsafe.Pointer
len int
cap int
}
三,初始化切片
初始化切片的方式有三种:
- 通过下标的方式获得数组或者切片的一部分;
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
slice1 := arr[1:3]
slice2 := arr[:5]
slice3 := arr[3:]
- 使用字面量初始化新的切片;
sloce1:= []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
slice2 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
- 使用关键字
make
创建切片:
slice1 := make([]int, 3)
slice2 := make([]int, 3, 10)
使用Make创建切片时,在类型检查期间会将测创建数据的类型,调用对应的函数。在这不过度展开,后续再分享Make创建切片的逻辑。
三,切片扩容机制
调用append给切片添加元素,使用append后调用的是runtime包下slice.go中的growslice函数
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < newLen {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = newLen
}
}
}
...
}
以上代码是扩容的规则:
- 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容
- (当新切片需要的容量cap小于两倍)当原slice容量<threshold的时候,新slice容量变成原来的2倍
- 当原slice容量>threshold,进入一个循环,每次容量增加(旧容量+3*threshold) /4
以上扩容规则是1.18版本之后的。
1.18版本之前:
threshold = 1024,循环扩容的公式为扩大1.25倍。
四,切片的拷贝
切片的拷贝分为浅拷贝和深拷贝
浅拷贝
因为切片是一个引用类型在用等号进行传递的时候是传递了一个指向数组的指针值,两个切片共享同一个底层数组。
func main() {
slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
slice2 := slice1
fmt.Println(slice1[1])
fmt.Println(slice2[1])
slice2[1] = 1000
fmt.Println(slice1[1])
fmt.Println(slice2[1])
}
上述代码slice1和slice2输出的值一样。
深拷贝
调用copy函数时,会创建一个行的地址,获取老slice的值,这两个slice不共享一个底层数组。
func main() {
slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
slice2 := make([]int, 10)
copy(slice2, slice1)
fmt.Println(slice1[1])
fmt.Println(slice2[1])
slice2[1] = 1000
fmt.Println(slice1[1])
fmt.Println(slice2[1])
}
上述代码slice1和slice2输出的值不一样。
Copy和for遍历拷贝哪个性能好
答案是Copy的性能好。
for遍历拷贝是切片值一个一个访问赋值获得的。
Copy函数的底层会调用memmove()
整块拷贝内存,是用汇编实现的,因此性能很好。