切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。
1.slice结构
首先来看 slice的结构 第一个array是一个指向底层数组首位的指针,第二个len是数字的长度,也就是数组当前有多少个元素,第三个cap是数组的容量。cap永远会大于等于len
array unsafe.Pointer
len int // 切片的长度 通过len(s)获取
cap int // 切片的容量 ,通过cap(s)获取
}
2.slice的创建方式
1.使用字面量初始化切片:
s := []int{1, 2, 3, 4, 5}
这种方式直接声明一个切片并通过大括号将元素值初始化。
2.使用make函数创建指定长度和容量的切片:
s := make([]int, 5)
这里的make([]int, 5)创建了一个长度为5的切片,并将其初始化为int类型的零值。
3.使用make函数创建指定长度和容量的切片,并预分配内存空间:
s := make([]int, 5, 10)
这里的make([]int, 5, 10)创建了一个长度为5,容量为10的切片。它会预先分配10个元素大小的内存空间,以便在后续添加元素时可以更高效地操作。
4.创建空切片:
var s []int
s := make([]int, 0)
这里的[]int和make([]int, 0)声明了一个未初始化的空切片。需要注意的是,空切片和nil切片是不同的,空切片是一个已分配但没有任何元素的切片,在操作前需要先分配空间
无论使用哪种方式创建切片,都可以根据需要进行后续的元素添加、删除等操作。
3.slice是值传递
slice在做为参数传递的时候是值传递,列如:
func Test_slice(t *testing.T) {
s := []int{1,2}
fmt.Printf("切片的地址:%p\n", &s)
Zhi(s)
}
func Zhi(s []int) {
fmt.Printf("切片的地址:%p\n", &s)
}
输出为:
切片的地址:0xc000008138
切片的地址:0xc000008168
可以发现两个数组的地址是不同的,slice在作为参数传递的时候,会拷贝一份传递过去
那这里引入一个问题
func Test_slice(t *testing.T) {
s1 := []int{1, 2}
Zhi(s1)
fmt.Printf("切片的地址:%p\n,%v", &s1, s1)
}
func Zhi(s2 []int) {
s2[0] = 2
s2[1] = 1
fmt.Printf("切片的地址:%p\n,%v", &s2, s2)
}
1.这个时候两个数组打印出的元素会是什么?
切片的地址:0xc000008150
,[2 1]
切片的地址:0xc000008138
,[2 1]
按理来说如果是值传递,那么s1打印的应该是[1,2],s2但是的是[2,1],但是很奇怪s1,s2打印的都是[2,1]
这是因为虽然slice是值传递,但是他是浅拷贝,他拷贝的是slice结构体里面三个字段的值,而第一个字段存的是指向数组首位的指针,在拷贝之后依旧指向的是同一个内存地址
导致两个数组实际上是使用的同一片空间,所以在更改s2的值之后,s1也会发生变化
那么对代码进行些许改动这个时候s1,s2输出的又会是什么
func Test_slice(t *testing.T) {
s1 := []int{1, 2}
Zhi(s1)
fmt.Printf("切片的地址s1:%p\n,%v", &s1, s1)
}
func Zhi(s2 []int) {
s2 = append(s2, 1)
s2[0] = 2
s2[1] = 1
fmt.Printf("切片的地址s2:%p\n,%v", &s2, s2)
}
切片的地址s2:0xc000008150
,[2 1 1]
切片的地址s1:0xc000008138
,[1 2]
可以发现对s2添加一个元素之后,s2发生了变化,但是s1没有发生变化
4.slice扩容
func Zhi(s2 []int) {
fmt.Println(cap(s2))
s2 = append(s2, 1)
fmt.Println(cap(s2))
s2[0] = 2
s2[1] = 1
fmt.Printf("切片的地址s2:%p\n,%v", &s2, s2)
}
还是上面的代码,在append的前后打印数组的容量,发现数组的容量发生了变化,从2变到了4,扩大了两倍,这是因为这个时候len == cap ,slice没有容量存放新过来的元素,所以s2进行了扩容以便有足够的空间来存放新来的元素。下面是扩容的主要代码:
// runtime/slice.go
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
// 如果 oldCap小于256直接进行翻倍扩容
if oldCap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
// 在cap大于256的时候对slice进行扩容
// 进行循环扩容直接内存分配的内存足够
for 0 < newcap && newcap < newLen {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
// 扩容规则,随着newcap的增加,增长倍数会慢慢的到1.25,使增长变化更加平滑
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}
}
s := make([]int, 512)
fmt.Println(cap(s))
s = append(s, 1)
fmt.Println(cap(s))
输出
512
848
但是看这一段代码,如果按照 (newcap + 3*threshold) / 4扩容应该是 832,但是扩容实际到了848,这是因为在计算新切片的容量的时候,会根据切片的元素类型大小来做一些优化。
部分代码为
switch {
case et.Size_ == 1:
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.Size_ == goarch.PtrSize: // 元素类型是8个字节
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) // 进行内存优化,也就是内存对齐,获取实际要分配的内存
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize) // 计算真正的扩容后容量
case isPowerOfTwo(et.Size_):
var shift uintptr
if goarch.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
} else {
shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
}
lenmem = uintptr(oldLen) << shift
newlenmem = uintptr(newLen) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
capmem = uintptr(newcap) << shift
default:
lenmem = uintptr(oldLen) * et.Size_
newlenmem = uintptr(newLen) * et.Size_
capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.Size_)
capmem = uintptr(newcap) * et.Size_
}
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize { // _MaxSmallSize = 32768 也就是4096 当size小于4096时
if size <= smallSizeMax-8 { // smallSizeMax= 1024 也就是当size<=127时
// size_to_class8数组用来获取class_to_size数组的索引
// divRoundUp(size, smallSizeDiv),相当于计算ceil(size / 8)
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}