go Slice 详解
使用
新建方法
变量方式
语法
a:=[]T{}
var c []T
T 指的是变量类型
demo
package main
import "fmt"
func main() {
//声明一个 int 型切片
a := []int{}
//声明并初始化一个 int 型切片,长度为4,容量为4
b := []int{1, 2, 3, 4}
//声明一个未指定大小的数组来定义切片
var c []int
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
make
语法
sliceName:=make([]T,lenSlice,CapSlice)
package main
import "fmt"
func main() {
//声明一个 int 型切片
a:=make([]int,4,4)
fmt.Println(a)
}
数组剪切
语法
slice := array[l,r,cap]
slice
是一个基于array
(或一个切片)的切片,从索引l
开始,到索引r-1
结束(即r
不包含在切片中),并且其容量为cap
。(左闭右开)
cap 字段可以不填写,即
slice := array[l,r]
此时的cap=len(slice)
demo
package main
import "fmt"
func main() {
//声明一个 int 型切片
A := [5]int{1, 2, 3, 4, 5}
a := A[1:2:3]
fmt.Println(a)
}
函数
len() && cap()
用法示例
--code
a := []int{1, 2, 3, 4}
fmt.Println(len(a))
fmt.Println(cap(a))
--result
4
4
Copy()
demo
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}
使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。
例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。
也就是说,copy过程中不会发生扩容。
append()
slice= append(slice,element1,element2,element3...)
demo
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
其中需要注意的是
s = append(s, s2...)
因为 s2 是一个元素集而不是单个元素,因此需要注意加上...
遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历。
func main() {
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
for index, value := range s {
fmt.Println(index, value)
}
}
删除元素
go 并没有为 slice 写单独的 delete 函数,而是通过组合切片来进行元素的删除的。
demo
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
利用左闭右开的特性就对了。
是否为空
结论
仅使用 len(s) == 0
作为判断标准。
验证
运行下列代码
package main
import "fmt"
func main() {
a := []int{}
fmt.Println("a:", a)
fmt.Println("&a:", &a)
fmt.Println("&a == nil:", &a == nil)
fmt.Println("a == nil:", a == nil)
fmt.Println("len(a) == 0:", len(a) == 0)
var b []int
fmt.Println("b:", b)
fmt.Println("&b:", &b)
fmt.Println("&b == nil:", &b == nil)
fmt.Println("b == nil:", b == nil)
fmt.Println("len(b) == 0:", len(b) == 0)
}
结果
a: []
&a: &[]
&a == nil: false
a == nil: false
len(a) == 0: true
b: []
&b: &[]
&b == nil: false
b == nil: true
len(b) == 0: true
a,b同样是空切片,但是两者在测试 == nil
的时候的结果却不相同
解释
切片是依赖于底层数组实现的,因此切片并不是没有地址的,即使是空切片也是指向一个空数组,而一个空数组是有地址的。
b 还不是一个切片,此时他只是一个被声明的结构体罢了,还没有存储数据,因此才会是 nil
记住:每个切片都指向一个底层数组。
跟官方的说法是:
节选自官方源码注释
// p = unsafe.Pointer(u + offset)
// Note that the pointer must point into an allocated object, so it may not be nil.
简要翻译一下就是,unsafe.Pointer 不可以是一个空指针
而 Slice 的结构是
type slice struct { array unsafe.Pointer len int cap int }
因此 slice 不会为 nil
原理
结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
- array 用于指向一个底层数组
- len 是目前切片存储的数据长度(个数)
- cap 是底层数组所能容纳是数据长度(个数)
使用make创建Slice
使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。
例如,语句slice := make([]int, 5, 10)
所创建的Slice,结构如下图所示:
Slice 扩容(cap 发挥作用的重点)
基本原理
使用 append 向 Slice 追加元素时,如果 Slice 空间不足,将会触发 Slice 扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新 Slice,然后返回新 Slice,扩容后再将数据追加进去。
例如,当向一个 capacity 为5,且 length 也为5的Slice再次追加1个元素时,就会发生扩容,如下图所示:
而这个扩容的操作其实是:
- 申请一个更大的底层数组
- 把原数据复制到新的数组中
- 把要添加的数据放到新数组中。
源码
//version go1.19.3
// growslice handles slice growth during append.
// growslice处理append期间的slice增长。
// It is passed the slice element type, the old slice, and the desired new minimum capacity,and it returns a new slice with at least that capacity, with the old data copied into it.
//它被传递slice元素类型、旧slice和所需的新最小容量,并且它返回一个至少具有该容量的新slice,旧数据被复制到其中。
// The new slice's length is set to the old slice's length,NOT to the new requested capacity.
//新切片的长度被设置为旧切片的长度,而不是新请求的容量。
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
// ... qian'm
//从这里开始是关键
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
//原本设计的容量足够容纳新slice
newcap = cap
} else {
const threshold = 256
//原本容量是否大于 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// 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 = cap
}
}
}
// ...
}
扩容规则
扩容容量的选择遵循以下规则:
flowchart TD
A["开始"] --> B{"cap>=newlen"}
B -->|true| C["直接插入原底层数组"] --> K
B -->|false| D["newLen>cap*2"]
D -->|false| E{cap<256}
D -->|true| M{"cap=newlen"} --> H
E -->|YES| F["cap*=2"] --> H["申请新的底层数组"]
E -->|NO| G["cap+=(cap+3*256)/4"] --> H
H --> I["复制旧数据到新数组中"] --> J["将数据插入到新底层数组中"] -->K
K{"update len"} --> L["返回当前底层数组"]
重点问题
此部分仅为个人复习用,请谨慎参考
-
slice 为空的判定方式,为什么这么用?
len(slice)==0 因为 nil 不能作为判断标准,详情参见上文。
-
slice 扩容的方式?
参见上文
-
slice 的 cap 应该如何设定?
尽量预估可能需要的开销,减少扩容造成的性能损失,但也不要占据过多的内存。
-
赋值拷贝和 copy 的区别?
赋值拷贝共享一个底层数组,copy 函数是重新创建了一个底层数组
注意点 OR 编程Tips
- 创建切片时可跟据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能;
- 切片拷贝时需要判断实际拷贝的元素个数
- 谨慎使用多个切片操作同一个数组,以防读写冲突
参考资料
- GO 专家编程(参考了许多图文)
- Go 语言基础之切片
- Go 语言切片扩容规则是扩容2倍?1.25倍?到底几倍