Go 语言切片(Slice)
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
你可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度。
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len) 也可以简写为 slice1 := make([]type, len)
也可以指定容量,其中 capacity 为可选参数。
make([]T, length, capacity)
这里 len 是数组的长度并且也是切片的初始长度。
切片初始化
s :=[] int {1,2,3 }
直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3。
s := arr[:]
初始化切片 s,是数组 arr 的引用。
s := arr[startIndex:endIndex]
将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
s := arr[startIndex:]
默认 endIndex 时将表示一直到arr的最后一个元素。
s := arr[:endIndex]
默认 startIndex 时将表示从 arr 的第一个元素开始。
s1 := s[startIndex:endIndex]
通过切片 s 初始化切片 s1。
s :=make([]int,len,cap)
通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片。
len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
以下为具体实例:
实例
package main
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
以上实例运行输出结果为:
len=3 cap=5 slice=[0 0 0]
空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
实例
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
以上实例运行输出结果为:
len=0 cap=0 slice=[] 切片是空的
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:
实例
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int,0,5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int,0,5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
执行以上代码输出结果为:
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8] numbers == [0 1 2 3 4 5 6 7 8] numbers[1:4] == [1 2 3] numbers[:3] == [0 1 2] numbers[4:] == [4 5 6 7 8] len=0 cap=5 slice=[] len=2 cap=9 slice=[0 1] len=3 cap=7 slice=[2 3 4]
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
实例
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
以上代码执行输出结果为:
len=0 cap=0 slice=[] len=1 cap=1 slice=[0] len=2 cap=2 slice=[0 1] len=5 cap=6 slice=[0 1 2 3 4] len=5 cap=12 slice=[0 1 2 3 4]
老逗先生
104***5037@qq.com
输出结果:
我们可以看出切片,实际的是获取数组的某一部分,len切片<=cap切片<=len数组,切片由三部分组成:指向底层数组的指针、len、cap。
老逗先生
104***5037@qq.com
LeonWilliam
257***8572@qq.com
sliceTest 函数是切片测试代码,简单的两种初始化方式。
twoDimensionsArray 函数是二维数组测试函数。
测试结果:
1.二维数组中的元素(一位数组)个数 > 限制的列数,异常;
2.二维数组中的元素(一位数组)个数 <= 限制的列数,正常;
假设列数为 3, 某个一位数组 {1}, 则后两个元素,默认为 0。
LeonWilliam
257***8572@qq.com
jean
333***2@qq.com
我们基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?
这里有个公式,对于底层数组容量是 k 的切片 slice[i:j] 来说:
例子:
执行结果:
jean
333***2@qq.com
amano
106***6790@qq.com
结果为:
capacity 为 7 是因为 number3 的 ptr 指向第三个元素, 后面还剩 2,3,4,5,6,7,8, 所以 cap=7。
如:
结果为:
capacity 为 4 是因为 number4 的 ptr 指向第六个元素, 后面还剩 5,6,7,8 所以 cap=4。
amano
106***6790@qq.com
ABC123
abc***c.com
合并多个数组:
输出结果:
ABC123
abc***c.com
lslxdx
lsl***@163.com
在做函数调用时,slice 按引用传递,array 按值传递:
输出结果为:
lslxdx
lsl***@163.com
berakcup
104***3071@qq.com
append() 和 copy() 部分,貌似有没说明白的地方。
当 numbers = [0, 1] 时,append(numbers, 2, 3, 4) 为什么 cap 从 2 变成 6 ?
经过实践得知,append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),如果大于那么 cap 不变,否则 cap 等于 max{cap(list), cap[params]},所以当 append(numbers, 2, 3, 4) cap 从 2 变成 6。
berakcup
104***3071@qq.com
andriyy
ano***ous@abc.com
使用 copy 函数要注意对于 copy(dst, src),要初始化 dst 的 size,否则无法复制。
错误示例:
输出结果:
正确示例:
输出结果:
andriyy
ano***ous@abc.com
Noah
lh_***9@126.com
实例:
以上代码执行输出结果为:
Noah
lh_***9@126.com
gibson1112
185***72536@163.com
切片内部结构:
第一个字段表示 array 的指针,是真实数据的指针第二个是表示 slice 的长度,第三个是表示 slice 的容量。
所以 unsafe.Sizeof(切片)永远都是 24。
当把 slice 作为参数,本身传递的是值,但其内容就 byte* array,实际传递的是引用,所以可以在函数内部修改,但如果对 slice 本身做 append,而且导致 slice 进行了扩容,实际扩容的是函数内复制的一份切片,对于函数外面的切片没有变化。
结果如下:
gibson1112
185***72536@163.com
ragonli
rag***ig@gmail.com
slice 的底层是数组指针,所以 slice a 和 s 指向的是同一个底层数组,所以当修改 s[0] 时,a 也会被修改。
ragonli
rag***ig@gmail.com
乱码kun
ly1***657282@gmail.com
当append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),如果大于那么 cap 不变,否则 cap = 2 * max{cap(list), cap[params]},所以当 append(numbers, 2, 3, 4) cap 从 2 变成 6。
前面已有相关详解,很可惜关键部分把“2 * ”给漏了,在此补充。
乱码kun
ly1***657282@gmail.com
Aaron_kitty
235***8645@qq.com
关于cap为何变为6的问题,我认为以上解释均为错,你们可以试试添加7个元素
cap就是10.
cap*2小于len(list) + len([params])则cap = len(list) + len([params])+1,反之cap=cap*2
查阅官方文档说
切片一旦初始化,就总是伴随着一个包含其元素的基本数组。 因此,切片与其数组及其它本数组的切片共享存储; 与此相反,不同的数组总是表示其不同的存储。
切片的基本数组可扩展其切片的结尾。容量是该扩展的量度: 它是切片的长度和切片往后数组的长度之和;长度达到其容量的切片可通过从原切片 (§Slices)‘切下’一个新的来创建。 切片 a 的容量可使用内建函数 cap(a) 获取。
并未指出规律性,以上规律是我个人总结。
Aaron_kitty
235***8645@qq.com
疏林如有诉
191***4098@qq.com
我觉得前面关于 cap 为什么会变成6的问题的解答都不太正确。
当添加 6 个元素时按照上面的说法 cap 的值应该分别为 12,9;但是经过实践得到的 cap 值为 8,可见以上几种说法都不是很全面。
经过多个数据的实验我发现 cap 的值只能为偶数。
实验数据如下:
当起始cap为2时,添加2个元素时cap为4,添加3个元素时cap为6,添加4个元素时cap为6,添加5个元素时cap为8,添加6个元素时cap为8,添加7个元素时cap为10,添加8个元素时cap为10……
当起始cap为1时,添加2个元素时cap为4,添加3个元素时cap为4,添加4个元素时cap为6,添加5个元素时cap为6,添加6个元素时cap为8,添加7个元素时cap为8,添加8个元素时cap为10……
所以我总结出来的经验是,添加元素后当len[list]+len[params]>cap时;cap的值需要修改。
若len[list]+len[params]为偶数,则cap=len[list]+len[params]
若len[list]+len[params]为奇数,则cap=len[list]+len[params]+1
以上为个人总结经验。
疏林如有诉
191***4098@qq.com
Raindeavor
wyw***ele@163.com
关于 cap 为何变为 6 的问题,我认为楼上解释也不对,哈哈。
1、当同时添加多个元素时:
即 cap 始终为偶数。
2、当一个一个添加元素时:
即 cap 总是呈 2 倍的增加(也是偶数)。
以上规律是我观察输出后的个人总结,初学 Go,没查文档什么的,总结的有问题还请楼下指出,嘿嘿。
Raindeavor
wyw***ele@163.com
Eternity味道
271***298@qq.com
关于切片容量 cap,当切片容量不够时,cap 会自动扩容到 2 倍:
当一个一个元素追加到切片中,切片容量变化过程为:
Eternity味道
271***298@qq.com
go语言初学者
Bao***_snow@163.com
关于楼上所讨论的 cap 由 2-> 6,根据查阅文档,可以得出一个结论:通过 append() 函数向数组中添加元素,首先 cap 会以二倍的速度增长,如果发现增长 2 倍以上的容量可以满足扩容后的需求,那么 cap*2,否则就会看扩容后数组的 length 是多少 cap=length+1。
go语言初学者
Bao***_snow@163.com
bullet
jia***angyu@163.com
参考地址
以 s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 为例:
建议:做 slice 截取时建议用两个参数,尤其是从底层数组进行切片操作时,因为这样在进行第一次 append 操作时,会给切片重新分配空间,这样减少切片对数组的影响。
结论:s = s[low : high : max] 切片的三个参数的切片截取的意义为 low 为截取的起始下标(含), high 为窃取的结束下标(不含 high),max 为切片保留的原切片的最大下标(不含 max);即新切片从老切片的 low 下标元素开始,len = high - low, cap = max - low;high 和 max 一旦超出在老切片中越界,就会发生 runtime err,slice out of range。另外如果省略第三个参数的时候,第三个参数默认和第二个参数相同,即 len = cap。
运行结果:
修改 max 值,发生越界错误:
执行后,错误信息如下:
bullet
jia***angyu@163.com
参考地址
da蘑菇大
860***289@qq.com
append 会让切片和与他相关的切片脱钩,但地址不变:
输出:
da蘑菇大
860***289@qq.com
网友小王
770***908@qq.com
为什么这里的 cap 不是 5 呢?
要弄清楚这个问题,先要理解如下代码:
以上代码执行输出结果为:
解释:每次cap改变的时候指向array内存的指针都在变化。当在使用 append 的时候,如果 cap==len 了这个时候就会新开辟一块更大内存,然后把之前的数据复制过去(实际go在append的时候放大cap是有规律的。在 cap 小于1024的情况下是每次扩大到 2 * cap ,当大于1024之后就每次扩大到 1.25 * cap 。所以上面的测试中cap变化是 1, 2, 4, 8)。
网友小王
770***908@qq.com
是小飞呀
211***3262@qq.com
参考地址
可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
从上面的代码可以看出以下内容:
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如
int
和string
类型的处理方式就不一样。是小飞呀
211***3262@qq.com
参考地址
dopsie
850***208@qq.com
楼上对于扩容后的容量说明都在发生扩容的代码逻辑中,没有说明这段代码中的参数 cap(申请容量) 是怎么计算的 。
经过多次的测试,我这边猜测是在 append 发生扩容时,申请容量是 原切片容量 + 追加的长度,如果是单数则 +1,再将这个容量传入扩容的方法进行判断,以讨论的 cap 变为6的代码为例:
原切片为 []int{1, 2} ,长度为2(记为 len1 = 2),容量为2(记为 cap1 =2)。
追加的长度为 {3, 4, 5},长度为3(记为 len2= 3),此时进行扩容,申请容量为6(记为 cap2 = cap1+len2,cap2 为单数,则+1,cap2 = 6),将 cap2 带入楼上的扩容判断逻辑。
申请容量 cap2 大于2倍的旧容量(cap1 = 2),则newcap = cap2 = 6
部分测试打印如下:
dopsie
850***208@qq.com
多喝热水
610***322@qq.com
关于 append 会让切片与其他相关切片脱钩的问题:
输出结果:
猜测脱钩的情况是由于切片底层数组扩张(创建了新数组替换旧数组)导致。
多喝热水
610***322@qq.com
vivisol
viv***l@foxmail.com
@da蘑菇大
提供了一个很好的例子,但是这个例子只在 cap(b)≤len(a) 的情况下成立;但是在 cap(b)>len(a) 时,append 并不能使切片脱钩。
先看看@da蘑菇大原始代码:运行结果为:
在 part3 中,通过 a[0]=1 修改了 a[0] 的值,但是 b[0] 的值并没有改变;通过 a=append(a,5) 增加了一个数据,b 切片没有增加数据;这只是在 b 的 cap 比较小的情况下才会出现的情况;如果 b 的 cap 足够大呢?
将代码修改成:
运行结果:
修改后的代码中,将 a, b 的 cap 都设置成 10;
在 part3 部分,通过 a[0] 修改 a[0] 值,b[0] 值也会跟着修改;通过 a=append(a, 5) 可以给 a 增加数据项,但是 b 的数据项并没有增加;
通过上面的试验可以发现,通过 b:=a 引用的方式,当 cap(b) 小于 len(a),修改 a[i] 的值并不会改变 b[i] 的值;但是如果 cap(b)≥len(a) 时,修改 a(i) 的值就会改变 b(i) 的值;
要想使两个切片同步改变,最好的方式是使用切片指针来实现,也就是上面的 *d。
vivisol
viv***l@foxmail.com