go slice 数组和切片使用区别示例解析
作者:eleven26 发布时间:2023-06-22 04:07:16
slice
(切片)是 go 里面非常常用的一种数据结构,它代表了一个变长的序列,序列中的每个元素都有相同的数据类型。 一个 slice
类型一般写作 []T
,其中 T
代表 slice
中元素的类型;slice
的语法和数组很像,但是 slice
没有固定长度。
数组和切片的区别
数组有确定的长度,而切片的长度不固定,并且可以自动扩容。
数组的定义
go 中定义数组的方式有如下两种:
指定长度:
arr := [3]int{1, 2, 3}
不指定长度,由编译器推导出数组的长度:
arr := [...]{1, 2, 3}
上面这两种定义方式都定义了一个长度为 3 的数组。正如我们所见,长度是数组的一部分,定义数组的时候长度已经确定下来了。
切片的定义
切片的定义方式跟数组很像,只不过定义切片的时候不用指定长度:
s := []int{1, 2, 3}
在上面定义切片的代码中,我们可以看到其实跟数组唯一的区别就是少了个长度。 那其实我们可以把切片看作是一个无限长度的数组。 当然,实际上它并不是无限的,它只是在切片容纳不下新的元素的时候,会自动进行扩容,从而可以容纳更多的元素。
数组和切片的相似之处
正如我们上面看到的那样,数组和切片两者其实非常相似,在实际使用中,它们也是有些类似的。
比如,通过下标来访问元素:
arr := [3]int{1, 2, 3}
// 通过下标访问
fmt.Println(arr[1]) // 2
s := []int{1, 2, 3}
// 通过下标访问
fmt.Println(s[1]) // 2
数组的局限
我们知道了,数组的长度是固定的,这也就意味着如果我们想往数组里面增加一个元素会比较麻烦, 我们需要新建一个更大的数组,然后将旧的数据复制过去,然后将新的元素写进去,如:
// 往数组 arr 增加一个元素:4
arr := [3]int{1, 2, 3}
// 新建一个更大容量的数组
var arr1 [4]int
// 复制旧数组的数据
for i := 0; i < len(arr); i++ {
arr1[i] = arr[i]
}
// 加入新的元素:4
arr1[3] = 4
fmt.Println(arr1)
这样一来就非常的繁琐,如果我们使用切片,就可以省去这些步骤:
// 定义一个长度为 3 的数组
arr := [3]int{1, 2, 3}
// 从数组创建一个切片
s := arr[:]
// 增加一个元素
s = append(s, 4)
fmt.Println(s)
因为数组固定长度的缺点,实际使用中切片会使用得更加普遍。
重新理解 slice
在开始之前,我们来看看 slice
这个单词的意思:作为名词,slice
的意思有 片;部分;(切下的食物)薄片;,作为动词,slice
的意思有 切;把…切成(薄)片; 的意思。 从这个角度出发,我们可以把 slice
理解为从某个数组上 切下来的一部分(从这个角度看,slice
这个命名非常的形象)。我们可以看看下图:
在这个图中,A
是一个保存了数字 1~7
的 slice
,B
是从 A
中 切下来的一部分,而 B
只包含了 A
中的一部分数据。 我们可以把 B
理解为 A
的一个 视图,B
中的数据是 A
中的数据的一个 引用,而不是 A
中数据的一个 拷贝 (也就是说,我们修改 B
的时候,A
中的数据也会被修改,当然会有例外,那就是 B
发生扩容的时候,再去修改 B
的话就影响不了 A
了)。
slice 的内存布局
现在假设我们有如下代码:
// 创建一个切片,长度为 3,容量为 7
var s = make([]int, 3, 7)
s[0] = 1
s[1] = 2
s[2] = 3
fmt.Println(s)
对应的内存布局如下:
说明:
slice
底层其实也是数组,但是除了数组之外,还有两个字段记录切片的长度和容量,分别是len
和cap
。上图中,
slice
中的array
就是切片的底层数组,因为它的长度不是固定的,所以使用了指针来保存,指向了另外一片内存区域。len
表明了切片的长度,切片的长度也就是我们可以操作的下标,上面的切片长度为3
,这也就意味着我们切片可以操作的下标范围是0~2
。超出这个范围的下标会报错。cap
表明了切片的容量,也就是切片扩容之前可以容纳的元素个数。
切片容量存在的意义
对于我们日常开发来说,slice
的容量其实大多数时候不是我们需要关注的点,而且由于容量的存在,也给开发者带来了一定的困惑。 那么容量存在的意义是什么呢?意义就在于避免内存的频繁分配带来的性能下降(容量也就是提前分配的内存大小)。
比如,假如我们有一个切片,然后我们知道需要往它里面存放 1w 个元素, 如果我们不指定容量的话,那么切片就会在它存放不下新的元素的时候进行扩容, 这样一来,可能在我们存放这 1w 个元素的时候需要进行多次扩容, 这也就意味着需要进行多次的内存分配。这样就会影响应用的性能。
我们可以通过下面的例子来简单了解一下:
// Benchmark1-20 100000000 11.68 ns/op
func Benchmark1(b *testing.B) {
var s []int
for i := 0; i < b.N; i++ {
s = append(s, 1)
}
}
// Benchmark2-20 134283985 7.482 ns/op
func Benchmark2(b *testing.B) {
var s []int = make([]int, 10, 100000000)
for i := 0; i < b.N; i++ {
s = append(s, 1)
}
}
在第一个例子中,没有给 slice
设置容量,这样它就只会在切片容纳不下新元素的时候才会进行扩容,这样就会需要进行多次扩容。 而第二个例子中,我们先给 slice
设置了一个足够大的容量,那么它就不需要进行频繁扩容了。
最终我们发现,在给切片提前设置容量的情况下,会有一定的性能提升。
切片常用操作
创建切片
我们可以从数组或切片生成新的切片:
注意:生成的切片不包含 end
。
target[start:end]
说明:
target
表示目标数组或者切片start
对应目标对象的起始索引(包含)end
对应目标对象的结束索引(不包含)
如:
s := []int{1, 2, 3}
s1 := s[1:2] // 包含下标 1,不包含下标 2
fmt.Println(s1) // [2]
arr := [3]int{1, 2, 3}
s2 := arr[1:2]
fmt.Println(s2) // [2]
在这种初始化方式中,我们可以省略 start
:
arr := [3]int{1, 2, 3}
fmt.Println(arr[:2]) // [1, 2]
省略 start
的情况下,就是从 target
的第一个元素开始。
我们也可以省略 end
:
arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]
省略 end
的情况下,就是从 start
索引处的元素开始直到 target
的最后一个元素处。
除此之外,我们还可以指定新的切片的容量,通过如下这种方式:
target[start:end:cap]
例子:
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s := arr[1:4:5]
fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4
往切片中添加元素
我们前面说过了,如果我们想往数组里面增加元素,那么我们必须开辟新的内存,将旧的数组复制过去,然后才能将新的元素加入进去。
但是切片就相对简单,我们可以使用 append
这个内置函数来往切片中加入新的元素:
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片
切片复制
go 有一个内置函数 copy
可以将一个切片的内容复制到另外一个切片中:
copy(dst, src []int)
第一个参数 dst
是目标切片,第二个参数 src
是源切片,调用 copy
的时候会把 src
的内容复制到 dst
中。
示例:
var a []int
var b []int = []int{1, 2, 3}
// a 的容量为 0,容纳不下任何元素
copy(a, b)
fmt.Println(a) // []
a = make([]int, 3, 3) // 给 a 分配内存
copy(a, b)
fmt.Println(a) // [1 2 3]
需要注意的是,如果 dst
的长度比 src
的长度小,那么只会截取 src
的前面一部分。
从切片删除元素
虽然我们往切片追加元素的操作挺方便的,但是要从切片删除元素就相对麻烦一些了。go 语言本身没有提供从切片删除元素的方法。 如果我们要删除切片中的元素,只有构建出一个新的切片:
对应代码:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
fmt.Println(a) // [1 2 3 4 5 6 7]
var b []int
b = append(b, a[:2]...) // [1 2]
b = append(b, a[5:]...) // [1 2 6 7]
fmt.Println(b) // [1 2 6 7]
在这个例子中,我们想从 a
中删除 3、4、5
这三个元素,也就是下标 2~4
的元素, 我们的做法是,新建了一个新的切片,然后将 3
前面的元素加入到这个新的切片中, 再将 5
后面的元素加入到这个新切片中。
最终得到的切片就是删除了 3、4、5
三个元素之后的切片了。
切片的容量到底是多少?
假设我们有如下代码:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3] 3 7
fmt.Println(s1, len(s1), cap(s1))
s2 := a[4:6]
// [5 6] 2 3
fmt.Println(s2, len(s2), cap(s2))
s1
和 s2
可以用下图表示:
s1
只能访问array
的前三个元素,s2
只能访问5
和6
这两个元素。s1
的容量是 7(底层数组的长度)s2
的容量是 3,从5
所在的索引处直到底层数组的末尾。
对于 s1
和 s2
,我们都没有指定它的容量,但是我们打印发现它们都有容量, 其实在切片中,我们从切片中生成一个新的切片的时候,如果我们不指定容量, 那新切片的容量就是 s[start:end]
中的 start
直到底层数组的最后一个元素的长度。
切片可以共享底层数组
切片最需要注意的点是,当我们从一个切片中创建新的切片的时候,两者会共享同一个底层数组, 如上图的那样,s1
和 s2
都引用了同一个底层的数组不同的索引, s1
引用了底层数组的 0~2
下标范围,s2
引用了底层数组 4~5
下标范围。
这意味着,当我们修改 s1
或 s2
的时候,原来的切片 a
也会发生改变:
var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)
s1 := a[:3]
// [1 2 3]
fmt.Println(s1)
s1[1] = 100
// [1 100 3 4 5 6 7]
fmt.Println(a)
// [1 100 3]
fmt.Println(s1)
在上面的例子中,s1
这个切片引用了和 a
一样的底层数组, 然后在我们修改 s1
的时候,a
也发生了改变。
切片扩容不会影响原切片
上一小节我们说了,切片可以共享底层数组。但是如果切片扩容的话,那就是一个全新的切片了。
var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
// a 容纳不下新的元素了,会进行扩容
b := append(a, 4)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 2 3]
fmt.Println(a)
// [1 100 3 4]
fmt.Println(b)
在上面这个例子中,a
是一个长度和容量都是 3
的切片,这也就意味着,这个切片已经满了。 在这种情况下,我们再往其中追加元素的时候,就会进行扩容,生成一个新的切片。 因此,我们可以看到,我们修改了 b
的时候,并没有影响到 a
。
下面的例子就不一样了:
// 长度为 2,容量为 3
var a = make([]int, 2, 3)
a[0] = 1
a[1] = 2
// [1 2] 2 3
fmt.Println(a, len(a), cap(a))
// a 还可以容纳新的元素,不用扩容
b := append(a, 4)
// [1 2 4] 3 3
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 100]
fmt.Println(a)
// [1 100 4]
fmt.Println(b)
在后面这个例子中,我们只是简单地改了一下 a
初始化的方式,改成了只放入两个元素,但是容量还是 3
, 在这种情况下,a
可以再容纳一个元素,这样在 b := append(a, 4)
的时候,创建的 b
底层的数组其实跟 a
的底层数组依然是一样的。
所以,我们需要尤其注意代码中作为切片的函数参数,如果我们希望在被调函数中修改了切片之后,在 caller 里面也能看到效果的话,最好是传递指针。
func test1(s []int) {
s = append(s, 4)
}
func test2(s *[]int) {
*s = append(*s, 4)
}
func TestSlice(t *testing.T) {
var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
test1(a)
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))
var b = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(b, len(b), cap(b))
test2(&b)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
}
在上面的例子中,test1
接收的是值参数,所以在 test1
中切片发生扩容的时候,TestSlice
里面的 a
还是没有发生改变。 而 test2
接收的是指针参数,所以在 test2
中发生切片扩容的时候,TestSlice
里面的 b
也发生了改变。
总结
数组跟切片的使用上有点类似,但是数组代表的是有固定长度的数据序列,而切片代表的是没有固定长度的数据序列。
数组的长度是类型的一部分,有两种定义数组的方式:
[2]int{1, 2}
、[...]int{1, 2}
。数组跟切片都可以通过下标来访问其中的元素,可以访问的下标范围都是
0 ~ len(x)-1
,x
表示的是数组或者切片。数组无法追加新的元素,切片可以追加任意数量的元素。
slice
的数据结构里面包含了:array
底层数组指针、len
切片长度、cap
切片容量。创建切片的时候,指定一个合适的容量可以减少内存分配的次数,从而在一定程度上提高程序性能。
我们可以从数组或者切片创建一个新的切片:
array[1:3]
或者slice[1:3]
。使用
append
内置函数可以往切片中添加新的元素。使用
copy
内置函数可以将一个切片的内容复制到另外一个切片中。切片删除元素没有好的办法,只能截取被删除元素前后的数据,然后复制到一个新的切片中。
假设我们通过
slice[start:end]
的方式从切片中创建一个新的切片,那么这个新的切片的容量是cap(slice) - start
,也就是,从start
到底层数组最后一个元素的长度。使用切片的时候需要注意:切片之间会共享底层数组,其中一个切片修改了切片的元素的时候,也会反映到其他切片上。
函数调用的时候,如果被调函数内发生扩容,调用者是无法知道的。如果我们不想错过在被调函数内切片的变化,我们可以传递指针作为参数。
来源:https://juejin.cn/post/7179159979193008188


猜你喜欢
- 一、查看存储过程存储过程创建以后,用户可以通过SHOW STATUS语句来查看存储过程的状态,也可以通过SHOW CREATE语句来查看存储
- SQLSRV驱动程序允许您创建一个结果集,其中包含可以根据游标类型以任何顺序访问的行。本主题将讨论客户端(缓冲)和服务器端(非缓冲)游标及其
- 为什么Python中0.2+0.1不等于0.3大家请看下面的python程序代码:print(0.2+0.1)猜一猜运行结果是什么,是0.3
- <!DOCTYPE html PUBLIC "-//W3C//DTD X
- 1、接口概述1)、什么是接口?接口是提供了一种用以说明一个对象应该具有哪些方法的手段。尽管它可以表明这些方法的语义,但它并不规定这些方法应该
- 经常会看到这种弹出层背景变暗的效果,感觉手痒于是自己写了一个基于jquery的弹出层类。我习惯先写好结构和样式,然后再写js交互效果。结构定
- 1、HTML模板和字符串模板HTML模板:直接在HTML页面挂载的模板。(即非字符串模板)非字符串模板:在单文件里用 <templat
- 前言用过unittest的童鞋都知道,有两个前置方法,两个后置方法;分别是setup()setupClass()teardown()tear
- asp之家注:学习javascript(js),免不了要用到打开新窗口,方法很多,总的来说是使用window.open。不同与HTML中的t
- 点阵字体是指根据文字的像素点来显示的字体,效果如下:使用Python读取并显示的过程如下:根据中文字符获取GB2312编码通过GB2312编
- 前言本文主要任务是使用通过 tf.keras.Sequential 搭建的模型进行各种花朵图像的分类,主要涉及到的内容有三个部分:使用 tf
- 背景在小站点上,直接用git来部署php代码相当方便,你的远程站点以及本地版本库都有一个版本控制,追踪问题或者回滚是很轻松的事情。因为在小公
- 1.数据的增删改查----------增加数据在视图函数中导入User模型类,然后使用下面的方法添加数据:from django.http
- 本文实例为大家分享了python实现自动打卡小程序的具体代码,供大家参考,具体内容如下"""湖南大学疫情防控每
- 面试题有一个test.xml文件,要求读取该文件中products节点的所有子节点的值以及子节点的属性值。test.xml文件:<!-
- 在 Pandas 中有很多种方法可以进行dataframe(数据框)的合并。本文将研究这些不同的方法,以及如何将它们执行速度的对比。合并DF
- 一、模块概述模块指的是包含python代码的文件,也就是一个.py文件就是一个模块。文件夹(directory)---->包(pack
- 如果用树作为索引的数据结构,每查找一次数据就会从磁盘中读取树的一个节点,也就是一页,而二叉树的每个节点只存储一条数据,并不能填满一页的存储空
- 相比于range,list等简易单词,enumerate仅凭外形都不太让人愿意用。事实上,enumerate还是很好用的。enumerate
- 一、查找操作1.Excel 模块 xlrd,xlwt,xlutils 分别负责 Excel 文件的读、写、读写转换工作!2.openpyxl