Go 知识slice

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里面
Go 知识slice_第1张图片

可以看到 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
Go 知识slice_第2张图片

这是最危险的情况,此时如果给 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)

使用程序验证
Go 知识slice_第3张图片

如何解决这种问题呢?
答案是指定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)

Go 知识slice_第4张图片

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))

Go 知识slice_第5张图片

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]int
    var 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 := s1
	s2 = 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]++
	}
}

Go 知识slice_第6张图片

解析:
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 := 5
	order := 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))
}

Go 知识slice_第7张图片

上述方式可以实现将切片一分为二,并且不会发生越界问题,怎么实现的呢?
首先创建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 := 5
	order := 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

Go 知识slice_第8张图片

你可能感兴趣的:(golang,go,切片,slice,go细节)