在前面的文章中,我和大家一起学习了一下关于 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 操作完成后,newSlice 会拥有一个全新的底层数组,这个数组的容量是原来的两倍。

函数 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?看这一篇就够了!相关推荐

  1. 搞懂RTK定位,看这一篇就够了

    搞懂RTK定位,看这一篇就够了! [导读]说到定位,相信大家一定不会觉得陌生.如今我们所处的信息时代,人人都有手机.每天,我们都会用到与地图和导航有关的APP. 这些APP,就是基于定位技术的.说到定 ...

  2. 程序员零基础转行Golang开发,看这一篇就够了(含30G自学教程笔记)

    Go 语言的发展越来越好了,很多大厂使用 Go 作为主要开发语言,也有很多人开始学习 Go,准备转 Go 开发. 那么,怎么学呢? 我发现,在互联网时代,学习的困难不是说没有资料,而是资料太多,不知道 ...

  3. 彻底搞清楚外贸流程,看这一篇就够了

    有一个朋友前几天跟我说:你看,网上这么多人做外贸,但有多少人真正知道外贸流程是什么样的? 可能是外贸做了很多年,已经不记得第一次做外贸的感觉了.但是最近接触了许多外贸新人,他们更加不清楚外贸的流程,一 ...

  4. 我就不信看完这篇你还搞不懂信息熵

    我就不信看完这篇你还搞不懂信息熵 https://mp.weixin.qq.com/s/7NrB0UtmELXD3UNO3C6jGA 让我们说人话!好的数学概念都应该是通俗易懂的. 信息熵,信息熵,怎 ...

  5. 面试还搞不懂Redis,快看看这40道面试题!| 博文精选

    作者| 程序员追风 责编 | Carol 出品 | CSDN云计算(ID:CSDNcloud) 近年来,微服务变得越来越热门,越来越多的应用部署在分布式环境中.常用的分布式实现方式之一就有 Redis ...

  6. 还搞不懂 Java NIO?快来读读这篇文章!

    来自:会点代码的大叔 首先,我们需要弄清楚几个概念:同步和异步,阻塞和非阻塞. 01 同步和异步 1. 同步 进程触发 IO 操作的时候,必须亲自处理: 比如你必须亲自去银行取钱. 2. 异步 进程触 ...

  7. 你还不会ElasticsSearch分页查询?那你看这一篇就够了,快拿走吧

    关注.星标下方公众号[ 大数据之美 ],和你一起成长 原文链接:你还不会ElasticsSearch分页查询?那你看这一篇就够了,快拿走吧 引言 我们使用mysql的时候经常遇到分页查询的场景,在my ...

  8. [Golang梦工厂]一个小项目带你学会GIN框架、JWT鉴权、swagger生成接口文档,看这一篇就够了

    前言 哈喽,大家好,我是asong,这是我的第八篇原创文章.听说你们还不会jwt.swagger,所以我带来一个入门级别的小项目.实现用户登陆.修改密码的操作.使用GIN(后台回复Golang梦工厂: ...

  9. 想要彻底搞懂“异地多活”,看完这篇就够了

    在软件开发领域,「异地多活」是分布式系统架构设计的一座高峰,很多人经常听过它,但很少人理解其中的原理. 异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的? 这些疑问,想必 ...

最新文章

  1. MySQL面试题 | 附答案解析(十八)
  2. C#设计模式——适配器模式(Adapter Pattern)
  3. Exchange2010各角色对软件环境的前提条件
  4. Lesson 16.1016.1116.1216.13 卷积层的参数量计算,1x1卷积核分组卷积与深度可分离卷积全连接层 nn.Sequential全局平均池化,NiN网络复现
  5. MVC应用程序显示RealPlayer(rm)视频
  6. ISDN与PSTN的区别是什么?
  7. 一个很cool的C#的高性能数学库
  8. python最后输出两位小数_人生苦短,我用python!
  9. 为Chrome添加Metro风格的快速拨号
  10. 【ICLR2020】看未知观测:一种简单的蒙特卡洛并行化方法
  11. 小米wifi驱动 linux,树莓派2B 安装小米wifi驱动
  12. 腾讯云即时通讯im之获取userSig
  13. 一篇很感人的DOTA小说--我本近卫
  14. 寒武纪MLU270安装运行Pytorch yolov3实录
  15. 将一个数的每一位都正序输出——简单算法
  16. ddos攻击是利用什么进行攻击
  17. 微信小程序----map组件实现检索【定位位置】周边的POI
  18. MissionPlanner的固件下载模块
  19. less和more的区别
  20. java图片处理工具类,很实用哦

热门文章

  1. Mac系统快速上手教学
  2. 计算机网络实验-交换机VLAN实验
  3. vscode跳转不到函数定义
  4. linux 大文件按照大小切割,关于linux系统下分割大文件的方法
  5. 远古VOD系统相关操作
  6. Pytorch 分类模板
  7. JAVA反射中的Accessible
  8. 诺基亚的驱动力:研发与产业族群
  9. RapidIO协议概述(一)
  10. 服务器硬件认识,关于服务器,你了解多少呢?