切片,长度是不固定的,当容量不足时,进行动态扩容,所以又叫动态数组。
数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
-
array:指向底层数组的指针
-
len:切片长度
-
cap:切片容量,即底层数组的长度
初始化
可以使用make和new初始化切片,区别是make返回切片本身,new返回的是切片的地址值。例子如下:
func initial() {
slice1 := make([]int, 5, 8)
slice2 := new([]int)
fmt.Println("slice1的类型是:", reflect.TypeOf(slice1))
fmt.Println("slice2的类型是:", reflect.TypeOf(slice2))
slice1 = append(slice1, 1)
*slice2 = append(*slice2, 1)
fmt.Println("slice1的值是:", slice1)
fmt.Println("slice2的值是:", *slice2)
}
执行结果:
切片值复制
Go语言中的切片在赋值、函数传参及函数返回值时也都是值复制。切片值复制指的是slice结构体的复制,因此指向底层数组的指针复制后仍然指向同一个数组地址。
func valueCopy() {
slice1 := make([]int, 1, 4)
slice2 := slice1
fmt.Printf("slice1的地址: %p, 值: %v\n", &slice1, slice1)
fmt.Printf("slice2的地址: %p, 值: %v\n", &slice2, slice2)
slice1[0] = 1
fmt.Println("修改slice1的值: ")
fmt.Printf("slice1的地址: %p, 值: %v\n", &slice1, slice1)
fmt.Printf("slice2的地址: %p, 值: %v\n", &slice2, slice2)
}
执行结果:
从执行结果中可以看出,slice1和slice2的地址值不同,是两个不同的切片,将slice1的第一个元素赋值为1,slice2的第一个元素的值也变成了1。说明slice1与slice2的底层数组是同一个。
切片扩容
切片可以动态扩容,当切片底层数组空间不足时,会触发扩容。重新分配一块更大的内存,将原来底层数组的数据复制到新分配的底层数组上,然后将新的数据追加进去。
使用append函数向切片追加元素,当容量不足时,调用src/runtime/slice.go的growslice函数,核心代码如下:
func growslice(et *_type, old slice, cap int) slice {
......
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
......
}
根据以上代码可以看出切片扩容的逻辑:
1,如果新申请的容量大于原来容量的2倍,则最终容量就是新申请的容量。
func capacity() {
// 新申请的容量大于原来容量的2倍
slice1 := make([]int, 1)
fmt.Printf("slice1的长度: %d, 容量: %d\n", len(slice1), cap(slice1))
slice1 = append(slice1, 1, 2)
fmt.Println("扩容后: ")
fmt.Printf("slice1的长度: %d, 容量: %d\n", len(slice1), cap(slice1))
}
执行结果:
slice1原来的容量是1,追加2个元素后,容量是3,大于原来容量的2倍,所以最终容量就是3。
2,如果原来的容量小于1024,且新申请的容量小于原来容量的2倍,则最终容量就是原来容量的2倍。
func capacity() {
// 原来的容量小于1024
slice2 := make([]int, 1)
fmt.Println("扩容前: ")
fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
slice2 = append(slice2, 1)
fmt.Println("第一次扩容后: ")
fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
slice2 = append(slice2, 2)
fmt.Println("第二次扩容后: ")
fmt.Printf("slice2的长度: %d, 容量: %d\n", len(slice2), cap(slice2))
}
执行结果:
3,如果原来的容量大于或等于1024,则最终容量是原始容量再增加原来的四分之一。由于涉及到内存对齐,所以 最终容量 >= 原始容量 + 原始容量*1/4。
func capacity() {
// 原来的容量大于或等于1024
slice3 := make([]int, 1024)
fmt.Println("扩容前: ")
fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
slice3 = append(slice3, 1)
fmt.Println("第一次扩容后: ")
fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
tmp := make([]int, 257)
slice3 = append(slice3, tmp...)
fmt.Println("第二次扩容后: ")
fmt.Printf("slice3的长度: %d, 容量: %d\n", len(slice3), cap(slice3))
}
执行结果:
slice3原始容量是1024,第一次扩容,追加一个元素后,容量变成了1024+1024/4=1280;第二次扩容,追加了257个元素后,容量变成了1696,大于1280+1280*1/4,进行了内存对齐。
总结
切片底层是结构体类型,有一个指向底层数组的指针,因此在传值时会指向同一个底层数组。
切片扩容的逻辑:
1,如果新申请的容量大于原来容量的2倍,则最终容量就是新申请的容量。
2,如果原来的容量小于1024,且新申请的容量小于原来容量的2倍,则最终容量就是原来容量的2倍。
3,如果原来的容量大于或等于1024,则最终容量是原始容量再增加原来的四分之一。由于涉及到内存对齐,所以 最终容量 >= 原始容量 + 原始容量*1/4。
更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号