切片无疑是 Go 语言中最重要的数据结构,也是最有趣的数据结构,它的英文词汇叫 slice。所有的 Go 语言开发者都津津乐道地谈论切片的内部机制,它也是 Go 语言技能面试中面试官最爱问的知识点之一。初级用户很容易滥用它,这小小的切片想要彻底的理解它是需要花费一番功夫的。在使用切片之前,我觉得很有必要将切片的内部结构做一下说明。

学过 Java 语言的人会比较容易理解切片,因为它的内部结构非常类似于 ArrayList,ArrayList 的内部实现也是一个数组。当数组容量不够需要扩容时,就会换新的数组,还需要将老数组的内容拷贝到新数组。ArrayList 内部有两个非常重要的属性 capacity 和 length。capacity 表示内部数组的总长度,length 表示当前已经使用的数组的长度。length 永远不能超过 capacity。

图片

上图中一个切片变量包含三个域,分别是底层数组的指针、切片的长度 length 和切片的容量 capacity。切片支持 append 操作可以将新的内容追加到底层数组,也就是填充上面的灰色格子。如果格子满了,切片就需要扩容,底层的数组就会更换。

形象一点说,切片变量是底层数组的视图,底层数组是卧室,切片变量是卧室的窗户。通过窗户我们可以看见底层数组的一部分或全部。一个卧室可以有多个窗户,不同的窗户能看到卧室的不同部分。

切片的创建

切片的创建有多种方式,我们先看切片最通用的创建方法,那就是内置的 make 函数

package main

import "fmt"

func main() {var s1 []int = make([]int, 5, 8)var s2 []int = make([]int, 8) // 满容切片
fmt.Println(s1)
fmt.Println(s2)
}

-------------
[0 0 0 0 0]
[0 0 0 0 0 0 0 0]

make 函数创建切片,需要提供三个参数,分别是切片的类型、切片的长度和容量。其中第三个参数是可选的,如果不提供第三个参数,那么长度和容量相等,也就是说切片的满容的。切片和普通变量一样,也可以使用类型自动推导,省去类型定义以及 var 关键字。比如上面的代码和下面的代码是等价的。

package main

import "fmt"

func main() {var s1 = make([]int, 5, 8)
s2 := make([]int, 8)
fmt.Println(s1)
fmt.Println(s2)
}

-------------
[0 0 0 0 0]
[0 0 0 0 0 0 0 0]

切片的初始化

使用 make 函数创建的切片内容是「零值切片」,也就是内部数组的元素都是零值。Go 语言还提供了另一个种创建切片的语法,允许我们给它赋初值。使用这种方式创建的切片是满容的。

package main

import "fmt"

func main() {var s []int = []int{1,2,3,4,5} // 满容的
fmt.Println(s, len(s), cap(s))
}

---------
[1 2 3 4 5] 5 5

Go 语言提供了内置函数 len() 和 cap() 可以直接获得切片的长度和容量属性。

空切片

在创建切片时,还有两个非常特殊的情况需要考虑,那就是容量和长度都是零的切片,叫着「空切片」,这个不同于前面说的「零值切片」。

package main

import "fmt"

func main() {var s1 []intvar s2 []int = []int{}var s2 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
fmt.Println(len(s1), len(s2), len(s3))
fmt.Println(cap(s1), cap(s2), cap(s3))
}

-----------
[] [] []0 0 00 0 0

上面三种形式创建的切片都是「空切片」,不过在内部结构上这三种形式是有差异的,甚至第一种都不叫「空切片」,而是叫着「 nil 切片」。但是在形式上它们几乎一摸一样,用起来差不多没有区别。所以初级用户可以不必区分「空切片」和「 nil 切片」,到后续章节我们会仔细分析这两种形式的区别。

切片的赋值

切片的赋值是一次浅拷贝操作,拷贝的是切片变量的三个域,你可以将切片变量看成长度为 3 的 int 型数组,数组的赋值就是浅拷贝。拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

package main

import "fmt"

func main() {var s1 = make([]int, 5, 8)// 切片的访问和数组差不多for i := 0; i < len(s1); i++ {
s1[i] = i + 1
}var s2 = s1
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))

// 尝试修改切片内容
s2[0] = 255
fmt.Println(s1)
fmt.Println(s2)
}

--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]

从上面的输出中可以看到赋值的两切片共享了底层数组。

切片的遍历

切片在遍历的语法上和数组是一样的,除了支持下标遍历外,那就是使用 range 关键字

package main

import "fmt"

func main() {var s = []int{1,2,3,4,5}for index := range s {
fmt.Println(index, s[index])
}for index, value := range s {
fmt.Println(index, value)
}
}

--------0 11 22 33 44 50 11 22 33 44 5

文章开头提到切片是动态的数组,其长度是可以变化的。什么操作可以改变切片的长度呢,这个操作就是追加操作。切片每一次追加后都会形成新的切片变量,如果底层数组没有扩容,那么追加前后的两个切片变量共享底层数组,如果底层数组扩容了,那么追加前后的底层数组是分离的不共享的。如果底层数组是共享的,一个切片的内容变化就会影响到另一个切片,这点需要特别注意。

package main

import "fmt"

func main() {var s1 = []int{1,2,3,4,5}
fmt.Println(s1, len(s1), cap(s1))

// 对满容的切片进行追加会分离底层数组var s2 = append(s1, 6)
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))

// 对非满容的切片进行追加会共享底层数组var s3 = append(s2, 7)
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
}

--------------------------
[1 2 3 4 5] 5 5
[1 2 3 4 5] 5 5
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6] 6 10
[1 2 3 4 5 6 7] 7 10

正是因为切片追加后是新的切片变量,Go 编译器禁止追加了切片后不使用这个新的切片变量,以避免用户以为追加操作的返回值和原切片变量是同一个变量。

package main

import "fmt"

func main() {var s1 = []int{1,2,3,4,5}append(s1, 6)
fmt.Println(s1)
}

--------------
./main.go:7:8: append(s1, 6) evaluated but not used

如果你真的不需要使用这个新的变量,可以将 append 的结果赋值给下划线变量。下划线变量是 Go 语言特殊的内置变量,它就像一个黑洞,可以将任意变量赋值给它,但是却不能读取这个特殊变量。

package main

import "fmt"

func main() {var s1 = []int{1,2,3,4,5}
_ = append(s1, 6)
fmt.Println(s1)
}

----------
[1 2 3 4 5]

还需要注意的是追加虽然会导致底层数组发生扩容,更换的新的数组,但是旧数组并不会立即被销毁被回收,因为老切片还指向这旧数组。

切片的域是只读的

我们刚才说切片的长度是可以变化的,为什么又说切片是只读的呢?这不是矛盾么。这是为了提醒读者注意切片追加后形成了一个新的切片变量,而老的切片变量的三个域其实并不会改变,改变的只是底层的数组。这里说的是切片的「域」是只读的,而不是说切片是只读的。切片的「域」就是组成切片变量的三个部分,分别是底层数组的指针、切片的长度和切片的容量。这里读者需要仔细咀嚼。

切割切割

到目前位置还没有说明切片名字的由来,既然叫着切片,那总得可以切割吧。切割切割,有些男娃子听到这个词汇时身上会起鸡皮疙瘩。切片的切割可以类比字符串的子串,它并不是要把切片割断,而是从母切片中拷贝出一个子切片来,子切片和母切片共享底层数组。下面我们来看一下切片究竟是如何切割的。

package main

import "fmt"

func main() {var s1 = []int{1,2,3,4,5,6,7}// start_index 和 end_index,不包含 end_index// [start_index, end_index)var s2 = s1[2:5]
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
}

------------
[1 2 3 4 5 6 7] 7 7
[3 4 5] 3 5

上面的输出需要特别注意的是,既然切割前后共享底层数组,那为什么容量不一样呢?解释它我必须要画图了,读者请务必仔细观察下面这张图

图片

我们注意到子切片的内部数据指针指向了数组的中间位置,而不再是数组的开头了。子切片容量的大小是从中间的位置开始直到切片末尾的长度,母子切片依旧共享底层数组。

子切片语法上要提供起始和结束位置,这两个位置都可选的,不提供起始位置,默认就是从母切片的初始位置开始(不是底层数组的初始位置),不提供结束位置,默认就结束到母切片尾部(是长度线,不是容量线)。下面我们看个例子

package main

import "fmt"

func main() {var s1 = []int{1, 2, 3, 4, 5, 6, 7}var s2 = s1[:5]var s3 = s1[3:]var s4 = s1[:]
fmt.Println(s1, len(s1), cap(s1))
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
fmt.Println(s4, len(s4), cap(s4))
}

-----------
[1 2 3 4 5 6 7] 7 7
[1 2 3 4 5] 5 7
[4 5 6 7] 4 4
[1 2 3 4 5 6 7] 7 7

细心的同学可能会注意到上面的 s1[:] 很特别,它和普通的切片赋值有区别么?答案是没区别,这非常让人感到意外,同样的共享底层数组,同样是浅拷贝。下面我们来验证一下

package main

import "fmt"

func main() {var s = make([]int, 5, 8)for i:=0;i<len(s);i++ {
s[i] = i+1
}
fmt.Println(s, len(s), cap(s))

var s2 = svar s3 = s[:]
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))

// 修改母切片
s[0] = 255
fmt.Println(s, len(s), cap(s))
fmt.Println(s2, len(s2), cap(s2))
fmt.Println(s3, len(s3), cap(s3))
}

-------------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8
[255 2 3 4 5] 5 8

使用过 Python 的同学可能会问,切片支持负数的位置么,答案是不支持,下标不可以是负数。

数组变切片

对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。

package main

import "fmt"

func main() {var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}var b = a[2:6]
fmt.Println(b)
a[4] = 100
fmt.Println(b)
}

-------
[3 4 5 6]
[3 4 100 6]

Go 语言还内置了一个 copy 函数,用来进行切片的深拷贝。不过其实也没那么深,只是深到底层的数组而已。如果数组里面装的是指针,比如 []*int 类型,那么指针指向的内容还是共享的。

func copy(dst, src []T) int

copy 函数不会因为原切片和目标切片的长度问题而额外分配底层数组的内存,它只负责拷贝数组的内容,从原切片拷贝到目标切片,拷贝的量是原切片和目标切片长度的较小值 —— min(len(src), len(dst)),函数返回的是拷贝的实际长度。我们来看一个例子

package main

import "fmt"

func main() {var s = make([]int, 5, 8)for i:=0;i<len(s);i++ {
s[i] = i+1
}
fmt.Println(s)var d = make([]int, 2, 6)var n = copy(d, s)
fmt.Println(n, d)
}
-----------
[1 2 3 4 5]2 [1 2]

当比较短的切片扩容时,系统会多分配 100% 的空间,也就是说分配的数组容量是切片长度的2倍。但切片长度超过1024时,扩容策略调整为多分配 25% 的空间,这是为了避免空间的过多浪费。试试解释下面的运行结果。

s1 := make([]int, 6)
s2 := make([]int, 1024)
s1 = append(s1, 1)
s2 = append(s2, 2)
fmt.Println(len(s1), cap(s1))
fmt.Println(len(s2), cap(s2))
-------------------------------------------7 121025 1344

扩容是一个比较复杂的操作,内部的细节必须通过分析源码才能知晓,不去理解扩容的细节并不会影响到平时的使用,所以关于切片的源码我们后续在高级内容里面再仔细分析。

原文发布时间为: 2018-11-12
本文作者:码洞
本文来自云栖社区合作伙伴“码洞”,了解相关信息可以关注“码洞”。

《快学 Go 语言》第 5 课 —— 神奇的切片相关推荐

  1. 快学 Go 语言 第 3 课 —— 分支与循环

    程序 = 数据结构 + 算法 上面这个等式每一个初学编程的同学都从老师那里听说过.它并不是什么严格的数据公式,它只是对一般程序的简单认知.数据结构是内存数据关系的静态表示,算法是数据结构从一个状态变化 ...

  2. 《快学 Go 语言》第 11 课 —— 千军万马跑协程

    协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少.通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允 ...

  3. 《快学 Go 语言》第 7 课 —— 冰糖葫芦串

    字符串通常有两种设计,一种是「字符」串,一种是「字节」串.「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的.Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字 ...

  4. go int 转切片_「快学 Go 语言」第 4 课——低调的数组

    数组就是一篇连续的内存,几乎所有的计算机语言都有数组,只不过 Go 语言里面的数组其实并不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换 ...

  5. 《快学 Go 语言》第 7 课 —— 诱人的烤串

    字符串通常有两种设计,一种是「字符」串,一种是「字节」串.「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的.Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字 ...

  6. 自学编程是从python语言还是c语言开始-非计算机专业大学生想自学编程应该学C语言还是学Python?...

    之前总结的文章,分享过来,希望对你有帮助.windliang:到底学哪一门编程语言​zhuanlan.zhihu.com 发展进程 学哪一门编程语言,我们不妨先梳理一下语言的发展过程. 机器语言 计算 ...

  7. 后端开发需要学什么_都2020年了,还在纠结学什么语言?| 后端篇

    几个礼拜前,一个学弟问我: "Ray,我打算之后要找工作了,不过现在自己没有特别深入的语言,最近想找一门好好学一下,你觉得学什么语言好呀?" 我表示:"这个要看你求职方向 ...

  8. 电气专业c语言要学得非常好吗,电气自动化专业需要学C语言吗?

    电气工程及其自动化专业要学C语言. 1.专业学位课程: 高等数学.电路原理.电子技术基础.微型计算机技术.计算机网络 .电机学.自动控制理论.电力系统分析.电力系统继电保护.C语言.C++. 2.专业 ...

  9. 重学c语言 新开导言

    重学c语言 导言 在我上大一的第一个学期,学院没给我们安排什么专业课内容.当时就学高数.线代.英语.思修之类的,闲暇时间很多,我就用课余时间在慕课上跟着浙江大学翁凯老师的c语言课学.在上大学之前我压根 ...

最新文章

  1. 《Engineering》评选2021年全球十大工程成就 | 中国工程院院刊
  2. python动态改变标签的颜色_PyQt4 treewidget 选择改变颜色,并设置可编辑的方法
  3. C 读写php,C语言读取文件所有内容
  4. 微型计算机原理及应用程序题,郑学坚《微型计算机原理及应用》(第4版)笔记和课后习题详解...
  5. Inna and Sequence
  6. MDOP套装之app-v安装使用及功能说明
  7. 计算机控制技术实际PID控制,计算机控制技术数字PID.doc
  8. CSAPP Computer System A Programmer Perspective
  9. VSTO项目的MSB3482错误
  10. bzoj 3165: [Heoi2013]Segment 线段树
  11. Mac技巧:如何使用macOS Big Sur中“通知中心”的小组件?
  12. Android 属性动画 常用方法 与 插值器 Interpolator
  13. 生成式对抗网络(GAN)相关问题汇总(较全面)
  14. canvas 圆角矩形填充_canva绘制圆角矩形
  15. 深度学习环境配置10——Ubuntu下的torch==1.7.1环境配置
  16. JSON--就是键值对
  17. 史上最全最详细多种手机主流操作系统详解
  18. 微信小程序上传文件功能实现
  19. PCI相关(2)- PCI桥与配置
  20. 腾讯云服务器网站504,使用腾讯、百度云CDN现403和504错误的解决及使用CDN踩的坑...

热门文章

  1. 1+1大于2 联想借东风破浪HPC市场
  2. angular2 step by step #1 - environment setup
  3. openStack 云平台管理节点管理网口流量非常大 出现丢包严重 终端总是时常中断问题调试及当前测试较有效方案...
  4. yum 快速搭建lnmp环境
  5. java------LinkedHashMap
  6. 禁止PHP警告性错误
  7. 免费资源:Typicons-免费图标字体
  8. IT菜鸟,希望大家赐教
  9. 用好VS2010扩展管理器
  10. 使用Hyperledger Ursa简化区块链安全性