首页 > 其他分享 >Go语言精进之路读书笔记第13条——了解切片实现原理并高效使用

Go语言精进之路读书笔记第13条——了解切片实现原理并高效使用

时间:2024-02-07 14:33:06浏览次数:29  
标签:13 slice 读书笔记 int cap len 切片 Go 元素

13.1 切片究竟是什么

Go数组是值语义的,这意味着一个数组变量表示的是整个数组,对于元素类型长度较大或元素个数较多的数组,如果直接以数组类型参数传递到函数中会有不小的性能损耗。

这时很多人会使用数组指针来定义函数参数,但在Go语言中,更地道的方式是使用切片。

切片之于数组就像是文件描述符之于文件。

数据结构

//slice的结构表示
//runtime/slice.go
type slice struct {
    array unsafe.Pointer //指向起点的地址
    len   int //切片长度
    cap   int //切片容量
}

• array:指向了内存空间地址的起点. 由于 slice 数据存放在连续的内存空间中,后续可以根据索引 index,在起点的基础上快速进行地址偏移,从而定位到目标元素
• len:切片的长度,指的是逻辑意义上 slice 中实际存放了多少个元素
• cap:切片的容量,指的是物理意义上为 slice 分配了足够用于存放多少个元素的空间. 使用 slice 时,要求 cap 永远大于等于 len

通过 slice 数据结构定义可以看到,每个 slice header 中存放的是内存空间的地址(array 字段),后续在传递切片的时候,相当于是对 slice header 进行了一次值拷贝,但内部存放的地址是相同的,因此对于 slice 本身属于引用传递操作

切片初始化

//第一种:声明但不初始化
//只是声明了 slice 的类型,但是并没有执行初始化操作,即 s 这个字面量此时是一个空指针 nil,并没有完成实际的内存分配操作,我们得到的是一个**nil切片**(也称作**零值切片**)
var s []int

//第二种:基于 make 进行初始化
//形式:make([]T,len,cap),需要保证 cap >= len
//在 index 为 [len, cap) 的范围内,虽然内存空间已经分配了,但是逻辑意义上不存在元素,直接访问会 panic 报数组访问越界
//但是访问 [0,len) 范围内的元素是能够正常访问到的,只不过会是对应元素类型下的零值
//需要注意,切片的长度一旦被指定了,就代表对应位置已经被分配了元素,尽管设置的会是对应元素类型下的零值
s11 := make([]int, 8) //cap不传的时候,默认等于len,即切片的长度 len 和 容量 cap 都为 8
s12 := make([]int, 8, 16) //表示已经在切片中设置了 8 个元素,会设置为对应类型的零值;cap = 16 代表为 slice 分配了用于存放 16 个元素的空间

//第三种:复合字面量的形式,初始化连带赋值
a := []int{1, 2, 3, 4, 5, 6} //长度和容量均为6
s2 := a[1:2:4] // a[start:end:max], 取到的是下标[start,end),len=end-start,cap=max-start, max不能超过原有切片的容量

fmt.Println(s11, s12, a, s2)

空切片说明

  • 空切片是已经被初始化但没有包含任何元素的切片。空切片的长度和容量都是0,但是它已经有了一个指向底层数组的指针,即使这个数组是空的
  • 空切片可以通过直接声明和初始化来创建,或者通过对nil切片调用内建的make函数或者切片截取
s := []int{} // 通过字面量创建空切片
s := make([]int, 0) // 通过make函数创建空切片
var arr [3]int
s := arr[:0] // 通过数组切片创建空切片
fmt.Println(s == nil) // 输出:false

切片的截取操作

  • 我们可以修改 slice 下标的方式,进行 slice 内容的截取,形如 s[a:b] 的格式
  • 其中 a b 代表切片的索引 index,左闭右开,比如 s[a:b] 对应的范围是 [a,b),代表的是取切片 slice index = a ~ index = b-1 范围的内容

此外,这里的 a 和 b 是可以缺省的:
• 如果 a 缺省不填则默认取 0 ,则代表从切片起始位置开始截取. 比如 s[:b] 等价于 s[0:b]
• 如果 b 缺省不填,则默认取 len(s),则代表末尾截取到切片长度 len 的终点,比如 s[a:] 等价于 s[a:len(s)]
• a 和 b 均缺省也是可以的,则代表截取整个切片长度的范围,比如 s[:] 等价于 s[0:len(s)]

13.2 切片的高级特性:动态扩容

切片初始化及赋值操作的正确示例

//例子一:元素追加
s := make([]int,0,5)
for i := 0; i < 5; i++{
    s = append(s, i)
}
// 结果为:
// s: [0,1,2,3,4]

//例子二:遍历
s := make([]int,5)
for i := 0; i < 5; i++{
    s[i] = i
}
// 结果为:
// s: [0,1,2,3,4]

append的时候,若是底层数组容量不足,会进行自动扩容

// len:4, cap: 4
s := []int{2,3,4,5}
// len:5, cap: 8    
s = append(s,6)

切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下:
• 倘若扩容后预期的新容量小于原切片的容量,则 panic
• 倘若切片元素大小为 0(元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回
• 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量
• 倘若老容量小于 256,则直接采用老容量的2倍作为新容量
• 倘若老容量已经大于等于 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止
• 结合 mallocgc 流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小
• 调用 mallocgc,对新切片进行内存初始化
• 调用 memmove 方法,将老切片中的内容拷贝到新切片中
• 返回扩容后的新切片

13.3 尽量使用 cap 参数

在创建切片时,尽量使用cap参数

补充1:元素删除

删除首尾元素

//删除首个元素
s := []int{0,1,2,3,4}
// [1,2,3,4]
s = s[1:]

//删除尾部元素
s := []int{0,1,2,3,4}
// [0,1,2,3]
s = s[0:len(s)-1]

删除中间的元素

//先截取待删除元素的左侧部分内容,然后追加上待删除元素后侧部分的内容
s := []int{0,1,2,3,4}
// 删除 index = 2 的元素
s = append(s[:2],s[3:]...)
// s: [0,1,3,4], len: 4, cap: 5

删除所有元素

s := []int{0,1,2,3,4}
s = s[:0]
// s: [], len: 0, cap: 5

补充2:切片拷贝

简单拷贝
对切片的字面量进行赋值传递 或 对切片进行截取

//创建出了一个新的 slice header 实例,但是其中的指针 array、容量 cap 和长度 len 仍和老的 slice header 实例相同
s := []int{0, 1, 2, 3, 4}
s1 := s

//s 和 s1 会使用同一片内存空间,只不过地址起点位置偏移了一个元素的长度. s1 和 s 的地址,刚好相差 8 个 byte
s := []int{0, 1, 2, 3, 4}
s1 := s[1:]

完整拷贝
slice 的完整复制,指的是会创建出一个和 slice 容量大小相等的独立的内存区域,并将原 slice 中的元素一一拷贝到新空间中

//slice 的完整复制可以调用系统方法 copy
//通过日志打印的方式可以看到,s 和 s1 的地址是相互独立的
s := []int{0, 1, 2, 3, 4}
s1 := make([]int, len(s))
copy(s1, s)
t.Logf("s: %v, s1: %v", s, s1)
t.Logf("address of s: %p, address of s1: %p", s, s1)

补充3:值传递还是引用传递?

//todo

标签:13,slice,读书笔记,int,cap,len,切片,Go,元素
From: https://www.cnblogs.com/brynchen/p/18002162

相关文章

  • golang之常用标准库汇总
    1.import"runtime/debug"func StackfuncStack()[]byteStack 返回格式化的go程的调用栈踪迹。 对于每一个调用栈,它包括原文件的行信息和PC值;对go函数还会尝试获取调用该函数的函数或方法,及调用所在行的文本。 func PrintStackfuncPrintStack()PrintStack将Stack......
  • 使用IDEA直接连接数据库报错:Server returns invalid timezone. Go to 'Advanced' tab
    错误详情:使用IDEA直接连接数据库报错:Serverreturnsinvalidtimezone.Goto'Advanced'tabandset'serverTimezone'propertymanually.错误原因:MySQL驱动中默认时区是UTC,与本地时间有时差。解决方案:点开最右侧导航栏Advanced,找到serverTimezone,在value处填写GMT保存再......
  • Maven3.9.6 构建项目报错 Failed to execute goal org.apache.maven.plugins:maven-re
    在使用Maven3.9.6构建项目时,出现以下错误:[INFO][INFO]---resources:3.3.1:resources(default-resources)@service-sample---[INFO]Copying18resourcesfromsrc/main/javatotarget/classes[INFO]Copying15resourcesfromsrc/main/resourcestotarget/classes[IN......
  • 洛谷P10136 暨 USACOJan2024S T3 题解
    题意简述原题已经很简了,没有什么简述的必要了。思维路径请注意本题解可以保证正确性但不保证如果有极端的Hack数据能够通过。拿到这道题上来的暴力想必是很容易的,即枚举每个\(L\)判断是否合法。接着我们就考虑优化,减少需要枚举的\(L\)的量。题目中要求余数最多有\(3\)......
  • CF1338C Perfect Triples 题解
    解题思路没什么好说的,就是打表找规律……表在这里不难发现,三元组中第一个数的最后两位按照\(00\to01\to10\to11\)的顺序变化,其他位也一样,同样,第二个数和第三个数中每两位分别按照\(00\to10\to11\to01\)和\(00\to11\to01\to10\)的顺序变化,且与第一个数对应变化......
  • CF1379C Choosing flowers 题解
    解题思路不是那么显然的,当只选一种\(b_i\)或全选\(a_i\)时最优。那么我们可以先对\(a_i\)从大到小排序,枚举每一个\(b_i\),然后二分找到第一个大于等于\(b_i\)的\(a_j\),判断\(a_1\sima_j\)中是否包含\(a_i\),如果包含,当前的答案为\(\displaystyle\left(\sum_{k=1}^{......
  • CF1385F Removing Leaves 题解
    解题思路简单贪心,优先选择叶子节点最多的,这样能够保证一定能把所有能删的都删了。因为要建一个可删除的图,所以我们可以使用set来存边,不然就需要维护一堆东西……那么我们肯定是从有叶子节点的点向父亲更新的,那么我们每次选择叶子节点最多的点,然后删除\(k\)个叶子,判断一下删......
  • CF1413C Perform Easily 题解
    解题思路其实是很简单的一道题,考虑计算出所有\(b_i\)在减去每一个\(a_j\)后所有可能的值,将这个值按照从小到大的顺序排序,那么我们可以考虑固定最小值,查找最大值,这个时候从最小值到最大值的区间内应该每一种\(b_i\)都出现了一次。那么,我们可以使用一个桶或者map搭配双指针......
  • 洛谷P10135 暨 USACOJan2024S T2 题解
    题意简述给点一棵有\(n\)个节点的树,在每个时间点都会在某个节点出现一瓶药水,记\(p_i\)为第\(i\)个时间点出现药水的节点,定义一次遍历为从\(1\)号节点走到任意节点,要求在遍历次数最少的情况下拾取最多数量的药水。思维路径首先我们要探讨遍历次数最少的状态是怎样的。由......
  • CF1338B Edge Weight Assignment 题解
    解题思路一种不需要树形dp的做法。因为是一颗无根树,所以我们不妨以重心为根。首先考虑边权最少的情况。可以发现这个时候对于任意两个叶子节点,我们可以分别考虑其到根节点的路径,这样对于求其路径的异或值是没有影响的,但是在这种情况下我们可以很方便的讨论其路径的奇偶性。令......