听说你还搞不懂Golang的Slice?看这一篇就够了!
在前面的文章中,我和大家一起学习了一下关于 Go 语言中数组的知识,当时有提到过一个知识点:在函数中传递数组是非常耗资源的一件事,所以更推荐大家使用切片(slice)来这么做。
那么切片又是一个怎样的东西呢?看完这篇文章你就知道了!
上一篇传送门:
初识切片
切片(slice)也是一种数据结构,它和数组非常相似,但它是围绕动态数组的概念设计的,可以按需自动改变大小。
切片的动态增长是通过内置函数 append 来实现的,这个函数可以快速且高效地增长切片。
使用切片后,不仅可以更方便地管理和使用数据集合,还可以在切片的基础上继续使用切片来缩小一个切片的长度范围。
因为切片的底层就是一个数组,所以切片和数组的一些操作类似。比如:获得切片索引、迭代切片等。
切片的对象非常小,它是一个只有 3 个字段的数据结构,分别是:
1.指向底层数组的指针2.切片的长度3.切片的容量
了解了一下切片的好处和特性之后,我们再来看看如何创建切片吧。
创建切片
创建切片的方式有两种,一种是使用内置的 make 函数,一种是使用切片的字面量。
下面我们来看一下这两种创建方式的使用方法。
使用 make 创建
在创建之前我们先简单地了解一下 make 函数的作用。
make 只能应用于三种数据类型:本文中的 slice、以及后面要说的 map 和 chan。
make 会为它们分配内存、初始化一个对应类型的对象并返回。注意,返回值的类型依然是被 make 的那个类型,make 只对其做了一个引用。
make 主要是用来做初始化的,记住这点,这在后面的学习中非常重要(但不是本章的后面)。
使用长度和容量声明整型切片
slice := make([]int, 4, 5)
在使用 make 创建切片的时候,一共可以传入三个参数:
1.声明一个 int 类型的切片2.指定切片的长度3.指定切片的容量,也就是底层数组的长度
由于切片可以按需改变大小,所以在声明类型的时候并不需要指定长度(即
[123]int
这种写法),也不需要让编译器自己去推断数组的大小(即[...]int
这种写法)。所谓的容量其实就是切片可以增加到的最大长度。如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。
长度和容量一样的情况
如果你在使用 make 函数创建切片的时候只使用了两个参数的话,那么这个切片的长度和容量将会是一样的。
例如这里我声明了一个 string 类型的切片,并将它的长度和容量都设置为 6。
slice := make([]string, 6)
注意:切片的容量不能小于其长度
slice: = make([]int, 3, 1)
这行代码在编译的时候是会报错的,因为切片的容量不能小于长度,需要注意。
通过字面量创建
// 创建一个字符串切片并赋值,其长度和容量都是5。
slice := []string {"I", "Love", "My", "祖", "国"} // 创建一个整型切片并赋值,其长度和容量都是3。
slice := []int {10, 20, 30}
另外,在使用切片字面量时,我们可以设置初始长度和容量。
// 创建一个整形切片,使用整形数字 8 初始化第 10 个元素
slice := []int {9: 8}
// 注:上面的这个 9 代表下标 9,也就是第 10 个元素
上面的代码表示声明一个长度和容量都为 10 的数组,并把第 10 个元素的值设定为 8,其他位置的元素此时因为没有赋值,所以是对应类型的零值。这里的话因为声明类型是 int 的关系,所以是一个 int 值的 0。
用哪种方式创建呢?
这个主要得看在声明切片的时候,是否知道里面部分元素的值。
如果不知道的话,不管使用哪种方式都可以。
但如果想在声明的时候顺带给元素赋值,那么就可以选择使用字面量的方式。
为什么切片会和底层数组有关系呢?
这个实际上和它的数据结构声明是有关系的,切片实际上是一个结构体类型的数据结构,看一下切片类型的源码就知道了:
type slice struct { array unsafe.Pointer // 底层数组 len int cap int
}
不过由于本文主要说的是切片的关系,结构体等内容就先暂且不谈了,现在我们只需要知道切片底层有个数组就可以了。
图解切片结构
下面通过一张图来对切片做一个直观的理解,就拿下面这个例子来说:
slice := make([]int, 4, 5)
slice[2] = 9
把它画成图的话就是这个样子的:
因为切片存的只是指向底层数组的指针而已,所以切片占用的内寸空间是很小的。
我们来做个简单的计算,我的电脑是 64 位的,unsafe.Pointer 类型变量占用 8 个字节,int 类型变量占用 8 个字节,所以算起来这个切片所占的内存空间大小只有 8 + 8 * 2 = 24 字节,非常小。
操作切片
通过切片创建切片
除了上面的方式以外,我们还可以使用切片来创建切片。
// 创建一个整型切片,其长度和容量都是 5 个元素
slice := []int{1, 2, 3, 4, 5} // 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
执行完这段操作以后,我们就有两个切片了,它们共享一个底层数组,我们通过一个图来帮助理解一下这个过程。
新切片的下标 [0]
对应的实际是底层数组的下标 [1]
。
如果没有限定容量的大小,那么可以得知:
•新切片的长度为 3-1=2。•新切片的容量为 5-1=4。(这个5代表原来切片总长度)
说到容量,还需要注意一点,它一般只是用来增加切片长度用的,我们无法通过下标去取里面的内容。
例如:
package main import "fmt" func main () { slice := make([]int, 4, 5) newSlice := slice[1:3] fmt.Println(newSlice[1]) fmt.Println(newSlice[2])
}
运行结果会是:
panic: runtime error: index out of range
使用切片创建新的切片的时候,实际上一共是有三个参数的,前面我们已经使用了两个,现在我们来看看第三个参数。
第三个参数是用来限定新的切片的最大容量的,这个最大容量计算是从索引位置开始,加上希望容量中包含的元素的个数得到的。
举个例子:
// 创建一个整型切片,其长度和容量都是 5 个元素
slice := []int{1, 2, 3, 4, 5} // 创建一个新切片,其长度为 3-1=2 个元素,容量为 4-1=3 个元素
newSlice := slice[1:3:4]
这里我们要获取的新切片要求是从底层数组索引 1 的位置开始,然后取其后面最多 3 个数。
执行后得到的就是我们要写的第三个参数:4。
此时如果用图表示的话是这个样子:
灰色部分是新切片不能拓展到的部分,原因很简单,因为我们把最大容量设置为了 3。
注意:如果设置的容量比可用的容量还大,就会得到一个语言运行时错误。
nil 切片
在声明切片时不做任何初始化,就会创建一个 nil 切片,nil 切片可以用于很多标准库和内置函数。
// 创建 nil 整型切片
var slice [] int
nil 切片长度为 0,容量也为 0。
那么这个东西有什么用呢?
比如说我们调用了一个函数,我们希望它返回一个切片,但是运行期间发生了异常,这个时候我们就可以通过判断结果是否为 nil,得知程序是否出现异常了。
切片赋值
对切片赋值就很简单了,直接通过它的下标进行赋值即可。举个例子:
slice := []int{10, 20, 30}
slice[1] = 3
得到结果如下:
[10,3,30]
注意:赋值的时候使用的索引,不能超过切片的最大索引,也就是切片的长度 - 1。
如果你需要给通过切片创建的切片进行赋值,那么你需要注意了!前面说过新旧切片是共享同一个底层数组的,而修改切片的值实际上是在修改底层数组的值,所以这就产生了一个问题,如果对新的切片赋值,那么旧的切片的值也会发生变化。
举个例子:
package main import "fmt" func main () { slice := make ([]int, 4, 5) newSlice := slice[1:3] fmt.Printf("旧切片赋值之前:% d\n", newSlice) fmt.Printf("新切片赋值之前:% d\n", slice) newSlice[1] = 666 fmt.Printf("旧切片赋值之前:% d\n", slice) fmt.Printf("新切片赋值之前:% d\n", newSlice)
}
得到结果如下:
新切片赋值之前:[0 0]
旧切片赋值之前:[0 0 0 0]
新切片赋值之后:[0 0 666 0]
旧切片赋值之后:[0 666]
看到了吗?旧切片的值也被改变了!那么同理,如果对旧切片赋值,新切片也是会发生变化,这个就不做演示了,和上面这个结果类似。
切片增长
Go 语言内置的 append 函数可以增加切片的长度,使用 append 需要一个被操作的切片和一个要追加的值。
append 必定会增加新切片的长度,而切片容量的变化则取决于被操作的切片的可用容量,可增长可不增长。
简单来说,如果 append 操作完之后,切片内的元素个数不大于容量数,那么新的切片就不增加容量。同样,此时的新切片还是和之前的切片共享同一个底层数组。但是这种做法我并不建议你使用!
举个例子:
package main import "fmt" func main() { slice := []int{1,2,3,4,5} newSlice := slice[1:3:4] fmt.Printf("新切片之前元素:% d\n", newSlice) fmt.Printf("旧切片之前元素:% d\n", slice) newSlice = append(newSlice, 6) fmt.Printf("旧切片增加之后元素:% d\n", slice) fmt.Printf("新切片此时元素:% d\n", newSlice)
得到结果如下:
新切片赋值之前:[2 3]
旧切片赋值之前:[1 2 3 4 5]
旧切片增加之后元素:[1 2 3 6 5]
新切片此时元素:[2 3 6]
让我们通过下面这张图来分析一下,上面都发生了什么。
看图可以发现,我们在增加新切片的元素的时候,无意中修改了底层的数组元素,这导致旧的切片的值也发生了变化,所以我更推荐你使用下面这种方式对新的切片进行元素的增加:
还记得前面说的:“使用切片创建切片的时候,可以使用第三个参数限制其最大容量”么?
这里我们要说的就是使用 append 之后,新的切片容量增大的情况。
如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,然后将被引用的现有的值复制到新数组里,再追加新的值。
也就是说,这种情况新的切片可以单独享用底层的数组,这样的话即使你修改了新切片的值,对旧的切片也不会造成任何影响,因为它们不再共享同一个底层数组。
举个例子:
slice := []int{1, 2, 3, 4, 6}
newSlice := append(slice, 6)
函数 append 会智能地处理底层数组的容量增长,在切片的容量小于 1000 个元素时,它总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长倍数就会被设为 1.25,也就是说会每次增加 25% 的容量。
append 除了可以添加元素以外,还可以在一个切片中追加另一个切片。只需要通过 ...
来将第二个切片的元素做一个拆分就行了,这里的 ...
就类似于 Python 里的解包操作。
s1 := []int {1, 2}
s2 := []int {3, 4} fmt.Printf("% v\n", append(s1, s2...)) 输出:
[1 2 3 4]
切片迭代
对了,切片还可以像数组那样去迭代,只需要这样就行了:
slice := []int {1, 2, 3, 4}
for index, value := range slice { fmt.Printf("Index: % d Value: % d\n", index, value)
}
在函数间传递切片
切片在函数间进行传递的时候,只是复制了切片的本身,不会涉及底层的数组。
前面我们计算了一下,在 64 位的机器上,切片仅占用了 24 个字节,而在函数间传递 24 字节的数据是非常简单、快速的事情。
这正是切片效率高的地方,不需要传递指针,也不需要处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本就可以了,非常的简单快捷。
参考资料
•https://www.jb51.net/article/126703.htm•https://www.cnblogs.com/chenpingzhao/p/9918062.html•https://blog.csdn.net/qq_19018277/article/details/100578553•http://www.meirixz.com/archives/80658.html
文章作者:「夜幕团队 NightTeam」 - 陈祥安
润色、校对:「夜幕团队 NightTeam」 - Loco
夜幕团队成立于 2019 年,团队包括崔庆才、周子淇、陈祥安、唐轶飞、冯威、蔡晋、戴煌金、张冶青和韦世东。
涉猎的编程语言包括但不限于 Python、Rust、C++、Go,领域涵盖爬虫、深度学习、服务研发、对象存储等。团队非正亦非邪,只做认为对的事情,请大家小心。
听说你还搞不懂Golang的Slice?看这一篇就够了!相关推荐
- 搞懂RTK定位,看这一篇就够了
搞懂RTK定位,看这一篇就够了! [导读]说到定位,相信大家一定不会觉得陌生.如今我们所处的信息时代,人人都有手机.每天,我们都会用到与地图和导航有关的APP. 这些APP,就是基于定位技术的.说到定 ...
- 程序员零基础转行Golang开发,看这一篇就够了(含30G自学教程笔记)
Go 语言的发展越来越好了,很多大厂使用 Go 作为主要开发语言,也有很多人开始学习 Go,准备转 Go 开发. 那么,怎么学呢? 我发现,在互联网时代,学习的困难不是说没有资料,而是资料太多,不知道 ...
- 彻底搞清楚外贸流程,看这一篇就够了
有一个朋友前几天跟我说:你看,网上这么多人做外贸,但有多少人真正知道外贸流程是什么样的? 可能是外贸做了很多年,已经不记得第一次做外贸的感觉了.但是最近接触了许多外贸新人,他们更加不清楚外贸的流程,一 ...
- 我就不信看完这篇你还搞不懂信息熵
我就不信看完这篇你还搞不懂信息熵 https://mp.weixin.qq.com/s/7NrB0UtmELXD3UNO3C6jGA 让我们说人话!好的数学概念都应该是通俗易懂的. 信息熵,信息熵,怎 ...
- 面试还搞不懂Redis,快看看这40道面试题!| 博文精选
作者| 程序员追风 责编 | Carol 出品 | CSDN云计算(ID:CSDNcloud) 近年来,微服务变得越来越热门,越来越多的应用部署在分布式环境中.常用的分布式实现方式之一就有 Redis ...
- 还搞不懂 Java NIO?快来读读这篇文章!
来自:会点代码的大叔 首先,我们需要弄清楚几个概念:同步和异步,阻塞和非阻塞. 01 同步和异步 1. 同步 进程触发 IO 操作的时候,必须亲自处理: 比如你必须亲自去银行取钱. 2. 异步 进程触 ...
- 你还不会ElasticsSearch分页查询?那你看这一篇就够了,快拿走吧
关注.星标下方公众号[ 大数据之美 ],和你一起成长 原文链接:你还不会ElasticsSearch分页查询?那你看这一篇就够了,快拿走吧 引言 我们使用mysql的时候经常遇到分页查询的场景,在my ...
- [Golang梦工厂]一个小项目带你学会GIN框架、JWT鉴权、swagger生成接口文档,看这一篇就够了
前言 哈喽,大家好,我是asong,这是我的第八篇原创文章.听说你们还不会jwt.swagger,所以我带来一个入门级别的小项目.实现用户登陆.修改密码的操作.使用GIN(后台回复Golang梦工厂: ...
- 想要彻底搞懂“异地多活”,看完这篇就够了
在软件开发领域,「异地多活」是分布式系统架构设计的一座高峰,很多人经常听过它,但很少人理解其中的原理. 异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的? 这些疑问,想必 ...
最新文章
- MySQL面试题 | 附答案解析(十八)
- C#设计模式——适配器模式(Adapter Pattern)
- Exchange2010各角色对软件环境的前提条件
- Lesson 16.1016.1116.1216.13 卷积层的参数量计算,1x1卷积核分组卷积与深度可分离卷积全连接层 nn.Sequential全局平均池化,NiN网络复现
- MVC应用程序显示RealPlayer(rm)视频
- ISDN与PSTN的区别是什么?
- 一个很cool的C#的高性能数学库
- python最后输出两位小数_人生苦短,我用python!
- 为Chrome添加Metro风格的快速拨号
- 【ICLR2020】看未知观测:一种简单的蒙特卡洛并行化方法
- 小米wifi驱动 linux,树莓派2B 安装小米wifi驱动
- 腾讯云即时通讯im之获取userSig
- 一篇很感人的DOTA小说--我本近卫
- 寒武纪MLU270安装运行Pytorch yolov3实录
- 将一个数的每一位都正序输出——简单算法
- ddos攻击是利用什么进行攻击
- 微信小程序----map组件实现检索【定位位置】周边的POI
- MissionPlanner的固件下载模块
- less和more的区别
- java图片处理工具类,很实用哦