Slice是什么
Slice也称为切片,切片其实就是建立在Go的数组之上的抽象类型,如果要理解切片,我们必须了解数组
对于Javaer来说,Slice就是Java的List,也就是动态数组
-
为什么有数组还需要切片?
数组长度在声明阶段已经固定,我们要想一个更大的数组,只能重新申请新数组,抛弃旧数组
在应对动态数据集合处理问题的时候,显得捉襟见肘,比如从网络中读取数据等场景,难以定义一个合适大小的数组
Slice的底层剖析 - 扩容机制
go
type slice struct {
array unsafe.Pointer // 底层数组指针(或者说是指向一块连续内存空间的起点)
len int // 长度
cap int // 容量
}
-
slice怎么做扩容的?
1.如果新切片的长度 > 旧切片容量的两倍,则新切片容量就为新切片的长度
2.如果旧切片的容量 < 256,那么新切片的容量就是旧切片的容量的两倍
3.如果旧切片的容量 > 256,那么新切片的容量会按照1.25倍的增速,直到 >= 新切片的长度
go
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
}
-
slice的内存对齐
切片的每一次扩容,都会去尝试进行内存对齐,举个例子,假设当前操作系统的内存对齐是8个字节
那么每一次内存对齐就是8 16 24 32 48 64 80 ...,那么如果你的下一次扩容的容量为60,那么最终分配会分配64
最终分配的mallogc的容量大小是 capmem,源码有点长,这里不贴出来了,感兴趣可以去 growslice() 看看
inip = mallocgc(capmem, nil, false)
切片通过函数传递,传的是什么?
传递的是切片的三个值 unsafe.Pointer、len、cap,这三个值,传到一个新的slice,但是指针指向的同一个底层数组
go
func main() {
s := make([]int, 5, 10)
PrintSliceStruct(&s)
test0(s)
}
func test0(s []int) {
PrintSliceStruct(&s)
}
func PrintSliceStruct(s *[]int) {
// 代码 将slice 转换成 reflect.SliceHeader
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
// 查看slice的结构
fmt.Printf("slice struct: %+v, slice is %v\n", ss, s)
}
输出结果可以看到,底层数组的地址都是一致的
ini
slice struct: &{Data:1374390779904 Len:5 Cap:10}, slice is &[0 0 0 0 0]
slice struct: &{Data:1374390779904 Len:5 Cap:10}, slice is &[0 0 0 0 0]
在函数里改变切片,函数外的切片会被影响吗?append()分析
这里需要分两种情况讨论,重点是切片的底层数组变不变
上面我们提到切片的传递是值传递,传递的是数组指针,既然都指向同一个内存地址,那里层函数修改肯定会导致外层修改的
这里就需要和Java的List做一个区分了,Java中的List在每次扩容的时候,都不会改变原数组指针的指向,Go则不同
- 切片底层数组不变的情况

- 切片底层数组变的情况

- 代码理解
go
func main() {
s := make([]int, 5)
PrintSliceStruct(&s)
case1(s)
case2(s)
}
// 底层数组不变
func case1(s []int) {
s[1] = 1
PrintSliceStruct(&s)
}
// 底层数组变化
func case2(s []int) {
s = append(s, 0)
s[1] = 1
PrintSliceStruct(&s)
}
func PrintSliceStruct(s *[]int) {
// 代码 将slice 转换成 reflect.SliceHeader
ss := (*reflect.SliceHeader)(unsafe.Pointer(s))
// 查看slice的结构
fmt.Printf("slice struct: %+v, slice is %v\n", ss, s)
}
输出结果
ini
slice struct: &{Data:1374390755376 Len:5 Cap:5}, slice is &[0 0 0 0 0]
slice struct: &{Data:1374390755376 Len:5 Cap:5}, slice is &[0 1 0 0 0]
slice struct: &{Data:1374390837248 Len:6 Cap:10}, slice is &[0 1 0 0 0 0]
切片截取
go
s := make([]int,5)
s1 = s[1:] // 截取0号元素以后的元素
s2 = s[1:3] // 截取[1,2]区间的元素 注意[1:3)左闭右开
s3 = s[:3] // 相当于[0,3) 截取 [0,2]区间的元素
这种切片截取是怎么做的?其实是重新创建了一个切片,但是旧切片和新切片共享同一个底层数组,和append()也是一样的逻辑
- 共享同一个底层数组

当新切片s2再进行append()的时候,此时就会触发扩容,并指向新的底层数组的地址,和上面的append()逻辑是一样的
代码题思考
输出结果是?
go
func main() {
doappend := func(s []int) {
s = append(s, 1)
printLenthAndCapacity(s)
}
s := make([]int, 8, 8)
doappend(s[:4])
printLenthAndCapacity(s)
doappend(s)
printLenthAndCapacity(s)
}
func printLenthAndCapacity(s []int) {
fmt.Println(s)
fmt.Printf("len=%d cap=%d \n", len(s), cap(s))
}
ini
0 0 0 0 1, len 5, cap 8
0 0 0 0 1 0 0 0, len 8, cap 8
0 0 0 0 1 0 0 0 1, len 9, cap 16
0 0 0 0 1 0 0 0, len 8, cap 8