Go 知识slice

Go 知识slice

  • [1. 什么是slice](#1. 什么是slice)
  • [2. slice 基础](#2. slice 基础)
    • [2.1 定义](#2.1 定义)
  • [2.2 实现原理](#2.2 实现原理)
    • [2.2.1 make 创建](#2.2.1 make 创建)
    • [2.2.2 切片 创建](#2.2.2 切片 创建)
  • [2.3 操作](#2.3 操作)
      • [2.3.1 append 追加](#2.3.1 append 追加)
      • [2.3.2 表达式切片](#2.3.2 表达式切片)
      • [2.3.3 扩展表达式](#2.3.3 扩展表达式)
      • [2.3.4 扩容](#2.3.4 扩容)
      • [2.3.5 拷贝](#2.3.5 拷贝)
  • [3. 测试一下](#3. 测试一下)
    • [3.1 len && cap](#3.1 len && cap)
    • [3.2 append && 扩容](#3.2 append && 扩容)
    • [3.3 切片表达式](#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

  • 切片

    Go 复制代码
    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 和原始数组共用底层函数。

Go 复制代码
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 的元素。

Go 复制代码
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 和数组就分离了

上述代码可以修改为:

Go 复制代码
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 追加

Go 复制代码
s := make([]int, 0) // 初始化长度为0的切片,注意空间不一定为0
s = append(s, 1) // 添加一个元素,长度为1
s = append(s, 2, 3, 4) // 添加多个元素
s = append(s, []{5, 6, 7}...} // 添加切片 

当切片空间不足的时候,会先扩容在追加.

原理:

首先 append 的函数定义

Go 复制代码
func append(slice []Type, elems ...Type) []Type

可以看出 append 接收一个切片和不定数量的元素,返回切片,需要注意的是切片的元素类型和需要添加的元素类型要相同。

而添加切片的时候,用到了切片展开 ...

用于将一个切片展开为一个个的元素。

2.3.2 表达式切片

表达式切片的格式 slice[low:high]

如果是数组0<=low<=gigh<=len(array)

如果是slice0<=low<=high<=cap(slice)

Go 复制代码
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)

比如

Go 复制代码
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

Go 复制代码
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 && 扩容

Go 复制代码
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]++
	}
}

解析:

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 切片表达式

Go 复制代码
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))
}

上述方式可以实现将切片一分为二,并且不会发生越界问题,怎么实现的呢?

首先创建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进行的,没有产生新的底层数组。

为了便于理解,我们加一个中间变量:

Go 复制代码
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))
}
Go 复制代码
lowOrder = &order[0], len=5, cap=5
midOrder = &order[5], len=5, cap=5
highOrder = &midOrder[0] = &order[5], len=5, cap=5
相关推荐
于顾而言几秒前
【笔记】Go Coding In Go Way
后端·go
qq_172805596 分钟前
GIN 反向代理功能
后端·golang·go
__AtYou__35 分钟前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
kevin_tech5 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
梦想画家6 小时前
快速解锁Rust Slice特性
开发语言·rust·slice
follycat7 小时前
2024强网杯Proxy
网络·学习·网络安全·go
幺零九零零8 小时前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s9 小时前
Golang--DOS命令、变量、基本数据类型、标识符
golang
__AtYou__9 小时前
Golang | Leetcode Golang题解之第541题反转字符串II
leetcode·golang·题解
OT.Ter9 小时前
【力扣打卡系列】单调栈
算法·leetcode·职场和发展·go·单调栈