Go 知识slice
- 1. 什么是slice
- 2. slice 基础
- 2.1 定义
- 2.2 实现原理
- 2.2.1 make 创建
- 2.2.2 切片 创建
- 2.3 操作
- 2.3.1 append 追加
- 2.3.2 表达式切片
- 2.3.3 扩展表达式
- 2.3.4 扩容
- 2.3.5 拷贝
- 3. 测试一下
- 3.1 len && cap
- 3.2 append && 扩容
- 3.3 切片表达式
1. 什么是slice
slice 是动态数组,依托数组实现,可以方便的进行扩容和传递,实际中比数组使用更加频繁。
2. slice 基础
2.1 定义
- 变量声明
var s []int
- 字面量
s1 := []int{}
,s2 := []int{1,2,3}
- 内置函数make
s1 := make([]int, 12)
指定长度 len
s2 := make([]int, 10, 20)
指定长度 len 和空间 cap - 切片
array := [5]int{1, 2, 3, 4, 5} s1 := array[0:2] // 从数组切片 [0,2) 长度2,下标 0, 1 s2 := s1[0:1] // 从slice切片 [0,1) 长度 1 ,下标 0 s3 := s2[:] // 从slice 切片,如果开始位置等于0,结束位置等于len,那么可以省略
2.2 实现原理
slice 的定义在src/runtime/slice.go
里面
可以看到 slice 有 len 和 cap 参数,里面记录了 array 数组的长度和空间。
所以 len(slice) 和 cap(slice) 的时间复杂度都是O(1)
2.2.1 make 创建
使用 make 创建会申请分配数组空间,然后将len和cap初始化赋值。
如果 make 没有指定 cap ,只指定了 len ,那么cap = len 。
2.2.2 切片 创建
使用切片创建的时候,slice 和原始数组共用底层函数。
array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := array[5:7]
在这种情况下 s 的底层数组 array = &array[0+init]
,len=2
,cap=len(array) - init=5
为什么 slice 的cap=10呢?
因为 slice 和 数组是共用底层数据的,此时计数的单位是从数组的0开始计数,但是操作 slice 的时候,会进行初始偏移量的处理。
比如 s[0] = array[0+5]
len(s) = 2
cap(s) = 10 -5
这是最危险的情况,此时如果给 s 进行追加元素,会修改 array 的元素。
array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
ts := array[5:7]
fmt.Println(array)
fmt.Println(ts)
ts = append(ts, -1)
fmt.Println(array)
fmt.Println(ts)
使用程序验证
如何解决这种问题呢?
答案是指定cap,这样在追加元素的时候,就会触发扩容,这样 slice 和数组就分离了
上述代码可以修改为:
array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
//ts := array[5:7]
//fmt.Printf("len=%d,cap=%d\n", len(ts), cap(ts))
//fmt.Println(array)
//fmt.Println(ts)
//ts = append(ts, -1)
//fmt.Println(array)
//fmt.Println(ts)nts := array[5:7:7]
fmt.Println(array)
fmt.Println(nts)
nts = append(nts, -1)
fmt.Println(array)
fmt.Println(nts)
2.3 操作
2.3.1 append 追加
s := make([]int, 0) // 初始化长度为0的切片,注意空间不一定为0
s = append(s, 1) // 添加一个元素,长度为1
s = append(s, 2, 3, 4) // 添加多个元素
s = append(s, []{5, 6, 7}...} // 添加切片
当切片空间不足的时候,会先扩容在追加.
原理:
首先 append 的函数定义
func append(slice []Type, elems ...Type) []Type
可以看出 append 接收一个切片和不定数量的元素,返回切片,需要注意的是切片的元素类型和需要添加的元素类型要相同。
而添加切片的时候,用到了切片展开 ...
用于将一个切片展开为一个个的元素。
2.3.2 表达式切片
表达式切片的格式 slice[low:high]
如果是数组0<=low<=gigh<=len(array)
如果是slice0<=low<=high<=cap(slice)
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[:] // 从数据转为 slice 省略了开始和结尾
s2 := arr[0:len(arr)] // 从数组转为 slice 指定了开始和结束
s3 := arr[2:4] // 从数组切片 由2开始到4结束,[2,4) => [3, 4] 取下标为 2, 3 的元素
s4 := arr[:4] // 从数组切片 由0开始到4结束,[0,4) => [1, 2, 3, 4] 取下标为 0, 1, 2, 3 的元素
s5 := arr[3:] // 从数组切片 由3开始到最后一个元素结束,[3, len(arr)) => [4, 5] 取下标为 3, 4 的元素
需要注意的是,表达式切片后产生的切片底层数组是共享的,所以非常容易出错。
2.3.3 扩展表达式
扩展表达式是为了解决新旧变量共用底层数组,导致相互影响的问题。只要限制了新slice的空间容量的限制,那么当修改的时候,还是会修改底层数组,也就是原数组的对应的元素也会发生变化,当时当发生扩容的时候,就不会完全修改超出预期的元素。
扩展表达式的格式slice[low:high:cap]
需要注意的是,cap是在原数组或者slice的基础上计算的cap值。
0<=low<=high<=cap<=cap(slice)
比如
array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
nts := array[5:7:7]
fmt.Println(array)
fmt.Println(nts)
fmt.Printf("len=%d, cap=%d\n", len(nts), cap(nts))
nts = append(nts, -1)
fmt.Println(array)
fmt.Println(nts)
fmt.Printf("len=%d, cap=%d\n", len(nts), cap(nts))
2.3.4 扩容
在slice扩容的过程中,会重新申请一个更大容量的底层数组,然后将len拷贝过去,将新的cap赋值。
扩容的规则:
- 如果原slice的cap<=1024,那么新slice的newCap = oldCap*2
- 如果原slice的cap > 1024,那么新slice的newCap = oldCap*1.25
2.3.5 拷贝
在前面说到,使用表达式切片,会导致底层数组共享的问题,使用扩展表达式,只是解决了新slice的越界修改的问题,最基本的数据隔离还是没有实现,修改新slice的数据,还是会影响到原slice的数据。
那么,如何解决呢,只能是主动显示的拷贝,产生新的底层数组,实现新旧slice的数据隔离。
使用内置函数copy
就能实现拷贝,但是需要注意,拷贝的过程中不会主动扩容。
意思是指假设新的slice的容量是5,旧的slice的容量是3,那么只会拷贝3个元素,实际上这种场景也到符合预期。
但是当newCap=5,oldCap=10,那么只会拷贝5个元素,并不会进行扩容。
3. 测试一下
3.1 len && cap
func TestSliceCap(t *testing.T) {var arr [10]intvar sl = arr[5:6]sl = append(sl, 7)fmt.Printf("len=%d\n", len(sl))fmt.Printf("cap=%d\n", cap(sl))
}
len=2
cap=5
3.2 append && 扩容
func TestSliceCap(t *testing.T) {s1 := []int{1, 2}s2 := s1s2 = append(s2, 3)add(s1)add(s2)fmt.Println(s1, s2)
}
func add(s []int) {s = append(s, 0)for i := range s {s[i]++}
}
解析:
s1 是slice
s2 等于s1,此时s1和s2相同
s2 = append(s2, 3) 发生了扩容,此时s2和s1是两个底层数组
add(s1)此时将s1传入了add函数,在add函数中进行append,进行了扩容,此时add函数内的s表示的底层数组也和s1不同了
add函数对newS1进行了+1操作,但是因为add函数没有返回,所以s1函数还是旧的底层数组,也就是[1 2]
add(s2)将s2传入了add函数,但是s2在s1的基础上进行了扩容,因为s1的cap<=1024,所以s2的cap=cap(s1)*2=4,
当进行append的时候,此时还不需要扩容,所以newS2=s2,add函数对元素+1也就修改了s2,也就是s2是[2 3 4 1]
打印的时候因为s2的len(s2)=3,所以没有打印s2[3]
相当于我们越界修改了len(s2)之外的数据,但是对于程序,s2[3]是不可见的.
3.3 切片表达式
func TestSliceExpress(t *testing.T) {orderLen := 5order := make([]int, 2*orderLen)lowOrder := order[:orderLen:orderLen]highOrder := order[orderLen:][:orderLen:orderLen]fmt.Printf("len(low)=%d, cap(low)=%d\n", len(lowOrder), cap(lowOrder))fmt.Printf("len(high)=%d,cap(high)=%d\n", len(highOrder), cap(highOrder))
}
上述方式可以实现将切片一分为二,并且不会发生越界问题,怎么实现的呢?
首先创建order,len=10,cap=10
因为orderLen=5,所以lowOrder采用扩展表达式切片:lowOrder,[0,5),而且指定cap=5,这样lowOrder就不会出现越界问题,当len=5并且需要扩容的时候,会进行拷贝。
highOrder相当于进行了两次切片:
第一次 order[orderLen:]表示从5开始切分,剩余的元素都要,但是没有设置cap
第二次 在第一次的基础上,进行了指定长度,但是因为第一次产生的变量中已经存在了init,所以第二次是从0开始。需要注意,上述所有操作都是基于order这个slice进行的,没有产生新的底层数组。
为了便于理解,我们加一个中间变量:
func TestSliceExpress(t *testing.T) {orderLen := 5order := make([]int, 2*orderLen)lowOrder := order[:orderLen:orderLen]midOrder := order[orderLen:]highOrder := midOrder[:orderLen:orderLen]fmt.Printf("len(low)=%d, cap(low)=%d\n", len(lowOrder), cap(lowOrder))fmt.Printf("len(mid)=%d,cap(mid)=%d\n", len(midOrder), cap(midOrder))fmt.Printf("len(high)=%d,cap(high)=%d\n", len(highOrder), cap(highOrder))
}
lowOrder = &order[0], len=5, cap=5
midOrder = &order[5], len=5, cap=5
highOrder = &midOrder[0] = &order[5], len=5, cap=5