slice 的底层数据就是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。
数组是定长的,长度定义好之后,不能再更改。因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。这是与 slice 不同的一点。
而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。
数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。
slice 的数据结构如下:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
nil切片 vs 空切片
nil切片:未初始化的切片(var s []int),len和cap均为0;- 空切片:已初始化但无元素(
s := make([]int, 0)),可用于JSON序列化空数组。
// 创建 nil 切片
var slice []int
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {0 0 0}
// 创建空切片
slice2 := make([]int,0)
slice3 := []int{}
fmt.Println(slice2,*(*reflect.SliceHeader)(unsafe.Pointer(&slice2))) // 输出:[] {18504816 0 0}
fmt.Println(slice3,*(*reflect.SliceHeader)(unsafe.Pointer(&slice3))) // 输出:[] {18504816 0 0}
// 创建零切片
slice4 := make([]int,2,5)
fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 输出:[0 0] {824634474496 2 5}
其实 nil slice 或者 empty slice 都是可以通过调用 append 函数来获得底层数组的扩容。最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice 或 empty slice,然后摇身一变,成为“真正”的 slice 了。
拷贝
copy() 函数的操作使用 append() 函数也能实现,只不过 append() 函数每次复制数据都需要创建临时切片,相比性能上 copy() 函数更胜一筹。
它可分为深拷贝与浅拷贝,我们一般默认值类型的数据是深拷贝,引用类型的数据是浅拷贝,那深拷贝与浅拷贝有什么区别呢?深拷贝是指使用数据时,基于原始数据创建一个副本,有独立的底层数据空间,之后的操作都在副本上进行,对原始数据没有任何影响。反之,操作对原始数据有影响的叫做浅拷贝,比如拷贝原始数组地址。
切片作为函数参数
slice 其实是一个结构体,包含了三个成员:len, cap, array。分别表示切片长度,容量,底层数据的地址。
当 slice 作为函数参数时,就是一个普通的结构体。其实很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。
值得注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,尽管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。 但是通过指向底层数据的指针,可以改变切片的底层数据,没有问题。
通过 slice 的 array 字段就可以拿到数组的地址。在代码里,是直接通过类似 s[i]=10 这种操作改变 slice 底层数组元素值。
另外,值得注意的是,Go 语言的函数参数传递,只有值传递,没有引用传递。
举例1:
package main
func main() {
s := []int{1, 1, 1}
f(s)
fmt.Println(s)
}
func f(s []int) {
// i只是一个副本,不能改变s中元素的值
/*for _, i := range s {
i++
}
*/
for i := range s {
s[i] += 1
}
}
程序输出:
[2 2 2]
果真改变了原始 slice 的底层数据。这里传递的是一个 slice 的副本,在 f 函数中,s 只是 main 函数中 s 的一个拷贝。在f 函数内部,对 s 的作用并不会改变外层 main 函数的 s。打印结果[2 2 2]是因为改变了底层数组的数据,而main中的s并没有变
举例2:
要想真的改变外层 slice,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。我们来看:
package main
import "fmt"
func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
return s
}
func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
}
func main() {
s := []int{1, 1, 1}
newS := myAppend(s)
fmt.Println(s)
fmt.Println(newS)
s = newS
myAppendPtr(&s)
fmt.Println(s)
}
运行结果:
[1 1 1]
[1 1 1 100]
[1 1 1 100 100]
myAppend 函数里,虽然改变了 s,但它只是一个值传递,并不会影响外层的 s,因此第一行打印出来的结果仍然是 [1 1 1]。
而 newS 是一个新的 slice,它是基于 s 得到的。因此它打印的是追加了一个 100 之后的结果: [1 1 1 100]。
最后,将 newS 赋值给了 s,s 这时才真正变成了一个新的slice。之后,再给 myAppendPtr 函数传入一个 s 指针,这回它真的被改变了:[1 1 1 100 100]。
实例分析:
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
运行结果:
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

接着,向 s2 尾部追加一个元素 100:
s2 = append(s2, 100)
s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

再次向 s2 追加元素200:
s2 = append(s2, 200)
这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

最后,修改 s1 索引为2位置的元素:
s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。

最重要的一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。
扩容
切片扩容原理

切片的底层结构由指针、长度和容量组成。当切片容量不足以容纳新增元素时,会触发扩容操作。扩容的核心逻辑由 growslice 函数实现
1、切片扩容,
使用的是专用函数append。在初始化之后,利用append在切片末尾添加元素时,将根据是否超出当前容量,进行扩容。其主要步骤包括:
-
检查所需容量:如果所需容量大于当前容量的两倍,则直接扩容到所需容量。
-
容量小于阈值时:如果当前容量小于阈值(默认 256),则新容量为当前容量的两倍。
-
容量大于阈值时:当容量超过阈值,扩容系数逐渐从 2 倍过渡到 1.25 倍,具体公式为:
newcap += (newcap + 3*threshold) >> 2
在源码中如下, threshold是256,>>2实现除4的效果,也就是说,大于阈值后,越靠近阈值,增长越趋近于2倍,越大于阈值,增长越趋近于1.25倍,由此,以实现容量的平滑增长。
2、内存对齐
上面得到的扩容之后的容量并不是最终结果,还有内存对齐,进一步调整newcap
小的对象按照sizeclass进行对齐,大对象按页对齐。
具体逻辑
在切片的扩容中,具体源码实现位于Go源码的runtime包中,具体路径为: src/runtime/slice.go。 如下,为了减小篇幅,此处只保留了关键部分:
growslice容量增长
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
oldLen := newLen - num
if raceenabled {
callerpc := sys.GetCallerPC()
racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(oldPtr, uintptr(oldLen*int(et.Size_)))
}
if asanenabled {
asanread(oldPtr, uintptr(oldLen*int(et.Size_)))
}
if newLen < 0 {
panic(errorString("growslice: len out of range"))
}
if et.Size_ == 0 {
return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}
// 计算新的容量
newcap := nextslicecap(newLen, oldCap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 判断元素类型大小,选择不同的计算方式,同时在以下内容中,对capmem的计算实际就是内存对齐之后的结果
noscan := !et.Pointers()
switch {
// 元素类型大小为1字节,直接使用元素数量作为字节数
case et.Size_ == 1:
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap), noscan)
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
// 元素类型大小为指针大小,用元素数量乘以指针大小作为字节数
case et.Size_ == goarch.PtrSize:
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
// 元素类型大小为2的幂,用位操作计算
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, noscan)
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, noscan)
newcap = int(capmem / et.Size_)
capmem = uintptr(newcap) * et.Size_
}
if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
// 根据元素类型,判断是否清理内存和触发写屏障
var p unsafe.Pointer
if !et.Pointers() {
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
}
}
memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}
}
nextslicecap 计算容量
以上源码中,主要实现的扩容操作是依据nextslicecap函数计算的,其源码如下:
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
// 新容量大于二倍旧容量,直接使用新容量
if newLen > doublecap {
return newLen
}
// 新容量小于阚值,使用二倍扩容
const threshold = 256
if oldCap < threshold {
return doublecap
}
// 大于阀值时,循环使用1.25倍扩容,直到满足要求,实现平滑增长
for {
// 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 += (newcap + 3*threshold) >> 2
// We need to check `newcap >= newLen` and whether `newcap` overflowed.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
if uint(newcap) >= uint(newLen) {
break
}
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
// 极端情况的溢出保护
if newcap <= 0 {
return newLen
}
return newcap
}
内存对齐
可见小对象按sizeclass对齐,大对象按页对齐。
// Returns size of the memory block that mallocgc will allocate if you ask for the size,
// minus any inline space for metadata.
func roundupsize(size uintptr, noscan bool) (reqSize uintptr) {
reqSize = size
if reqSize <= maxSmallSize-mallocHeaderSize {
// Small object.
if !noscan && reqSize > minSizeForMallocHeader { // !noscan && !heapBitsInSpan(reqSize)
reqSize += mallocHeaderSize
}
// (reqSize - size) is either mallocHeaderSize or 0. We need to subtract mallocHeaderSize
// from the result if we have one, since mallocgc will add it back in.
if reqSize <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(reqSize, smallSizeDiv)]]) - (reqSize - size)
}
return uintptr(class_to_size[size_to_class128[divRoundUp(reqSize-smallSizeMax, largeSizeDiv)]]) - (reqSize - size)
}
// Large object. Align reqSize up to the next page. Check for overflow.
reqSize += pageSize - 1
if reqSize < size {
return size
}
return reqSize &^ (pageSize - 1)
}
1537

被折叠的 条评论
为什么被折叠?



