Go 切片
切片结构
源码包 src/runtime/slice.go 中 定义 slice 的结构为
type slice struct {
array unsafe.Pointer
len int
cap int
}
array 指针指向底层数组
len 表示切片长度
cap 表示切片容量
切片扩容机制
go1.18之后:
growslice 方法中可以得知go切片扩容机制为:
1.当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
2.当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
3.当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4
// 截取growslice方法中部分代码
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
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 += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}
切片共享底层数组的特性
package main
import "fmt"
func main() {
var arr = make([]int, 3, 5) // [0 0 0] _ _
// arr brr 共享底层数组
brr := append(arr, 8) // [0 0 0 8] _
brr[0] = 1
fmt.Println(arr) //[1 0 0]
fmt.Println(brr) //[1 0 0 8]
}
母子切片内存共享与内存分离
package main
import "fmt"
func main() {
var arr = make([]int, 3, 5) // [0 0 0] _ _ // len=3 cap=5
s1 := arr[1:3] // 0 [0 0] _ _ // len=2 cap=4
// 母子切片内存共享
arr[1] = 1
fmt.Println(arr) // [0 1 0] _ _
fmt.Println(s1) // 0 [1 0] _ _
fmt.Println("------AAAAAAAAAAAAAAAAAAAAAAAAAA------")
// 母子切片 append 会把新元素放到 母切片预留空间中
s1 = append(s1, 8)
fmt.Println(arr) // [0 1 0] 8 _
fmt.Println(s1) // 0 [1 0 8] _
fmt.Println("------BBBBBBBBBBBBBBBBBBBBBBBBBB------")
// 此时仍然共享内存
arr[1] = 7
fmt.Println(arr) // [0 7 0] 8 _
fmt.Println(s1) // 0 [7 0 8] _
fmt.Println("------CCCCCCCCCCCCCCCCCCCCCCCCCC------")
// 当子切片(或母切片)不断执行append 耗完母切片预留空间 就会发生内存分离
s1 = append(s1, 9)
s1 = append(s1, 10)
fmt.Println(arr) // [0 7 0] 8 9
fmt.Println(s1) // [7 0 8 9 10]
fmt.Println("------DDDDDDDDDDDDDDDDDDDDDDDDDD------")
// 内存分离后
arr[1] = 4
fmt.Println(len(arr), cap(arr)) // len=3 cap=5
fmt.Println(len(s1), cap(s1)) // len=5 cap=8
fmt.Println(arr) // [0 4 0] 8 9
fmt.Println(s1) // [7 0 8 9 10]
}
package main
import "fmt"
func main() {
arr := []int{1, 2, 3, 4, 5, 6}
s1 := arr[:3]
s2 := arr[3:]
fmt.Println(s1, len(s1), cap(s1)) // s1的 len=3 cap=6
fmt.Println(s2, len(s2), cap(s2)) // s2的 len=3 cap=3
// 模拟对s1进行处理
foo(s1)
fmt.Println(s1) // [1 2 3]
fmt.Println(s2) // [11 5 6]
fmt.Println(arr) // [1 2 3 11 5 6]
// 以上代码中出现了2处与预期不符合的情况
// 1. 在foo()中既然能将sli切片(也就是s1)添加的第四位值(由10改为11)更改,
// 为何最后main()中的s1没有发生改变?
// 2. 对s1进行处理 却影响了s2的值?
// 第一点,在函数 foo() 中对 sli 进行了 append 操作,由于go值传递的原因,
// sli 是 s1 切片结构(切片结构:slice结构体,由指向底层数组的指针,切片的长度,切片的容量构成)的副本
// 因此,对 sli 的扩容不会影响到 s1 的切片结构,即 s1 的len、cap不会发生变化,所以切片s1不会发生变化。
// 第二点,值得注意的是 s1 的len=3,而cap=6,这意味着对s1进行 append 一个元素(foo中是10)不会发生扩容,
// 也就是 arr 和 s1 不会发生内存分离,他们仍然共用底层数组,sli指向数组的指针也没变,
// 所以当sli更改数组数据时,共用底层数组的切片都发生了改变,也就同时影响了 arr 和 s2。
}
func foo(sli []int) {
sli = append(sli, 10)
sli[3] = 11
}
返回切片会导致内存泄漏
func foo() []int {
// 假设 arr 是一个大容量切片
var arr = make([]int, 30000, 30000)
s1 := arr[1:3]
// s1 仍然持有底层数组
// 只要 s1 没有被gc回收, arr就一直得不到释放
return s1
}
在函数参数中使用切片指针
package main
import "fmt"
func main() {
var arr = make([]int, 3, 5) // [0 0 0] _ _
f1(&arr)
fmt.Println(arr) // [0 0 0 9] _
f2(&arr)
fmt.Println(arr) // [0 0 9] _ // len=3 cap=4
f3(arr)
fmt.Println(arr) // [1 0 9]
}
func f1(arr *[]int) {
// len 和 cap 要改变时,需要传切片指针 例如append 操作
*arr = append(*arr, 9)
}
func f2(arr *[]int) {
// 指向数组的指针变了
*arr = (*arr)[1:]
}
func f3(arr []int) {
// slice结构体里的3个 field 都不变 只改变底层数组,不需要传切片指针
arr[0] = 1
}