0 前言
slice
是一种灵活且强大的数据结构,它在功能上类似于其他编程语言中的数组,但提供了更多的灵活性。与数组不同,slice
允许动态调整长度,使其在大多数场景中更加适用。本文将深入解析 slice
的基本概念及底层实现原理,并通过分析一些面试中常见的易错题,加深对 slice
的理解。使用的go
版本1.22
1 基本概念
1.1 定义和初始化
slice
是基于数组构建的一个轻量级数据结构,拥有长度和容量两个属性,可以通过多种方式进行初始化。
// 直接初始化
s := []int{1, 2, 3, 4, 5}
// 使用make创建,指定长度和容量
s := make([]int, 3, 5) // [0 0 0]
// 使用数组切片 包含arr[1]到arr[3],但不包括arr[4]
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2 3 4]
1.2 长度和容量
- 长度 (
len
): 当前slice
中的元素个数。 - 容量 (
cap
): 从slice
的起始位置到底层数组末尾的元素个数。
s := []int{1, 2, 3, 4, 5}
fmt.Println(len(s)) // 输出: 5
fmt.Println(cap(s)) // 输出: 5
s2 := s[1:3]
fmt.Println(len(s2)) // 输出: 2
fmt.Println(cap(s2)) // 输出: 4 (底层数组从s[1]开始的元素数)
1.3 切片基本操作
1.3.1 访问和修改元素
与数组类似,slice
可以通过索引访问和修改元素
s := []int{1, 2, 3}
fmt.Println(s[1]) // 输出: 2
s[2] = 4
fmt.Println(s) // 输出: [1 2 4]
1.3.2 追加
使用内置函数 append
可以动态向 slice
追加元素。如果 slice
的容量不足以容纳新元素,append
会创建一个新的底层数组,并将原有数据复制到新数组中。
s := []int{1, 2, 3}
s = append(s, 4, 5)
fmt.Println(s) // 输出: [1 2 3 4 5]
1.4 总结
相信大家对切片这一数据结构已经不再陌生。前面介绍的只是 slice
的一些基本概念和操作。对于更高级的功能,或者使用中可能遇到的问题,比如扩容策略、函数内部对切片的修改是否会影响外部等,本文将在后面进行详细探讨。
2 底层实现原理
2.1 slice 数据结构
type slice struct {
// 指向了内存空间地址的起点. 由于 slice 数据存放在连续的内存空间中,后续可以根据索引 index,在起点的基础上快速进行地址偏移
array unsafe.Pointer
// 切片长度
len int
// 切片容量 >=len
cap int
}
2.2 创建切片
2.2.1 源码
先走一遍源码runtime/slice.go
再图解各种初始化的区别。
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 根据cap和每个元素类型大小,计算总容量
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
// 容量溢出或超限或长度小于0或长度大于容量,直接panic
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 进行内存分配和初始化
return mallocgc(mem, et, true)
}
2.2.2 make 指定len和cap 创建
通过make
函数创建了一个len=3
,cap=5
的切片。内存申请了5个int
大小的连续内存空间。但是后面两个暂时是访问不到的,slice[3]
会直接panic
的。index out of range [3] with length 3
2.2.3 直接初始化
直接初始化创建的是一个 len = 5
,cap = 5
的切片,这时候数组里面每个元素的值都初始化完成了。
2.2.4 使用数组切片
2.3 切片追加
在 Go 中,append
是一个用于向切片(slice
)中追加元素的内置函数。
s := []int{1, 2, 3}
s = append(s, 4, 5) //[1,2,3,4,5]
动态扩容
append
会根据需要自动扩展 slice
的容量。如果当前的 slice
容量足够大,append
会将新元素直接添加到现有的 slice
中;如果容量不足,append
会分配一个更大的底层数组,并将现有的元素复制到新的数组中,再添加新元素。扩容策略后面展开讲。
s := make([]int, 3, 5)
s = append(s, 4, 5)
fmt.Println(s) // [0 0 0 4 5]
fmt.Println(cap(s)) // 5
s = append(s, 6)
fmt.Println(s) // [0 0 0 4 5 6]
fmt.Println(cap(s)) // 10
追加另一个切片
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s1 = append(s1, s2...) // 将s2展开并追加到s1 [1,2,3,4,5,6]
函数内append
对外部slice
的影响
func modifySlice(s []int) []int {
s = append(s, 4) // 触发扩容了
s[0] = 100
fmt.Println(cap(s)) // 6
return s
}
func main() {
s := []int{1, 2, 3}
fmt.Println(s) // [1 2 3]
s2 := modifySlice(s)
fmt.Println(s) // [1 2 3]
fmt.Println(s2) // [100 2 3 4]
}
2.4 切片扩容
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap
doublecap := newcap + newcap
// 新的容量大于原容量的2倍,直接返回新的容量
if newLen > doublecap {
return newLen
}
const threshold = 256
// 原来容量小于256,则扩容后的容量为原容量的2倍
if oldCap < threshold {
return doublecap
}
for {
// 原容量*1.25+192
// 一直循环 直到满足
newcap += (newcap + 3*threshold) >> 2
if uint(newcap) >= uint(newLen) {
break
}
}
// 溢出直接取预期的新容量
if newcap <= 0 {
return newLen
}
return newcap
}
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
oldLen := newLen - num
if raceenabled {
callerpc := 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 {
// 当前切片大小为0,直接返回一个新的容量的切片
return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}
// 具体代码上面解释了
newcap := nextslicecap(newLen, oldCap)
//根据容量,计算所需的内存空间大小
var overflow bool
// lenmem 旧切片长度所占用的内存大小
// newlenmem 新切片长度所占用的内存大小
// capmem 新切片容量所需的内存大小
// noscan 切片元素是否包含指针
var lenmem, newlenmem, capmem uintptr
noscan := et.PtrBytes == 0
switch {
case et.Size_ == 1:
// 数组元素大小为1
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
// 函数计算需要的容量,并调整到合适的大小,将大小对齐到内存分配的 span class
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)
case isPowerOfTwo(et.Size_):
// 数组元素大小是2的指数,通过位运算进行空间大小的计算
var shift uintptr
if goarch.PtrSize == 8 {
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_
}
// 内存超出了可分配的范围 直接panic
if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
var p unsafe.Pointer
if et.PtrBytes == 0 {
// 无指针类型 直接mallocgc 分配内存
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
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}
}
扩容流程如下:
2.5 切片拷贝
切片拷贝(Slice Copy)指的是将一个切片的内容复制到另一个切片中。Go 提供了一些简单而高效的方式来进行切片的拷贝,主要通过内置的 copy
函数来实现
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
// 两个切片是独立的,修改不会相互影响
src[0]=100
dst[0]=10
fmt.Println("Copied elements:", n) // Copied elements: 3
fmt.Println("Source slice:", src) // Source slice: [100 2 3 4 5]
fmt.Println("Destination slice:", dst) // Destination slice: [10 2 3]
还有一种方式就是直接赋值的方式,这种方式实际底层还是指向的同一个地址,除非进行了扩容,否则修改原切片,复制的切片也是跟着修改
src := []int{1, 2, 3, 4, 5}
dst := src[1:]
src[1] = 100
fmt.Println("Source slice:", src) // 输出:Source slice: [1 100 3 4 5]
fmt.Println("Destination slice:", dst) // 输出:Destination slice: [100 3 4 5]
3 常见面试题案例
3.1 切片扩容和底层数组共享
修改一个切片的元素可能会影响其他共享相同底层数组的切片。
arr := []int{1, 2, 3, 4, 5}
s1 := arr[1:4] // s1 = [2, 3, 4]
s1[0] = 10
fmt.Println("Array:", arr) //[1 10 3 4 5]
问题: s1
和 arr
共享相同的底层数组,因此 s1
的修改会反映到 arr
中
3.2 切片扩容导致的不可预期行为
切片在扩容时,可能会重新分配底层数组,导致指向旧底层数组的切片失效。
s1 := []int{1, 2, 3}
s2 := s1
s1 = append(s1, 4, 5, 6)
fmt.Println("s1:", s1) // [1 2 3 4 5 6]
fmt.Println("s2:", s2) // [1 2 3]
问题:s1
扩容后,s2
仍指向旧的底层数组,因此 s2
不会看到扩容后的数据。
3.3 切片的容量和长度
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println("Length:", len(s)) // Length: 4
fmt.Println("Capacity:", cap(s)) // Capacity: 6 (根据扩容策略)
3.4 空切片的判定
var s1 []int
s2 := []int{}
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
3.5 切片切割
切片切割时,使用错误的索引或边界导致运行时错误。
s := []int{1, 2, 3, 4, 5}
fmt.Println(s[1:10]) // panic :slice bounds out of range [:10] with capacity 5
问题: 切片的切割操作超出了原切片的边界
3.6 切片传递到函数
修改切片的函数参数会影响原切片,扩容之后会指向一个新的底层数组。
func modifySlice(s []int) {
s = append(s, 20)
s[0] = 10
}
func main() {
s := []int{1, 2, 3}
modifySlice(s)
fmt.Println("Slice after function:", s) // Slice after function: [1 2 3]
}
问题:
append
操作没有影响到原切片的容量,因为 s
在函数内重新指向了一个新的底层数组。
3.7 越界访问
s := make([]int, 10, 12)
v := s[10]
问题: 会发生 panic,切片长度为10,容量为 12,判断是否越界以长度为准, index = 10 已经越界.
3.8 扩容策略
s := make([]int, 512)
s = append(s, 1)
fmt.Printf("len of s: %d, cap of s: %d", len(s), cap(s))// len of s: 513, cap of s: 848
这道题目实际上考察的是切片的扩容策略以及内存对齐的知识。我们可以先回顾一下之前提到的扩容流程。
当切片 s
的原始容量为 512 时,由于已经超过了 256 的阈值,因此扩容后的新容量计算为 512 * 1.25 + 192 = 832
。
在申请内存时,依据切片元素的大小乘以容量来计算所需的内存空间,这里的结果是 8 字节 * 832 = 6656
。
随后,根据内存对齐的原则,内存会被补齐到 6784 这个档次。这样计算后,6784 / 8 = 848
,因此最终的容量为 848。
如果您觉得有帮助,请关注我,另公众号【小张的编程世界】,如有任何错误或建议,欢迎指出。感谢您的阅读!
标签:Slice,int,fmt,len,切片,Println,slice,Go,解析 From: https://blog.csdn.net/luozong2689/article/details/141651890