概念

切片(slice)是对数组一个连续片段的引用,,所以切片是一个引用类型(更类似于 C/C++ 中的数组,Python 中的 list )。因为切片是引用,不需要使用额外的内存存储并且比数组更有效率,所以在 Go 中 切片比数组更常用。

构成

一个 slice 由三个部分构成:指针、长度和容量:

  • 指针:指向第一个 slice 元素对应的底层数组元素的地址(注意,slice 的第一个元素并不一定就是数组的第一个元素)
  • 长度对应 slice 中元素的数目,不能超过容量
  • 容量一般是从 slice 的开始位置到底层数组的结尾位置,内置的 lencap 函数分别返回 slice 的长度和容量。

创建

数组切片

Slice 本身没有数据,是对底层数组的 view。多个 slice 之间可以共享底层的数据,并且引用的数组部分区间可能重叠,即一个切片和相关数组的其他切片是共享存储的。相反,不同的数组总是代表不同的存储(数组实际上是切片的构建块)。

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}  // [0 1 2 3 4 5 6 7]
s := arr[2:6]                            // [2 3 4 5]// 共享存储
s[0] = 10
fmt.Println(arr)                         // [0 1 10 3 4 5 6 7]
fmt.Println((s))                         // [10 3 4 5]
复制代码

数组和 slice 之间有着紧密的联系,和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度,切片是一个 长度可变的数组。

声明赋值

切片与数组的类型字面量的唯一不同是不包含代表其长度的信息。因此,不同长度的切片值是有可能属于同一个类型的,不同长度的数组值必定属于不同类型。

s := []int{1, 2, 3}
复制代码

使用append

由于值传递的关系,必须接收 append 的返回值:

s = append(s, val)
s = append(s, val1,val2, val3)
s = append(s, slice...)
复制代码
var s []int
for i :=0; i<10; i++ {s = append(s, i)
}
fmt.Println(s)
复制代码

使用make

cap 是可选参数:make([]type, len, cap)

s1 := make([]int, 16)
s2 := make([]int, 10, 32)
复制代码

扩展阅读 —— new()make() 的区别: 两者都在堆上分配内存,但是它们的行为不同,适用于不同的类型:

  • new(T) 为类型 T 分配一片内存,初始化为 0 并且返回类型为 *T 的内存地址,返回一个指向类型为 T,值为 0 的地址的指针。适用于数组、结构体。
  • make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、mapchannel

也就是说,new 函数分配内存,make 函数初始化。

特性

长度len

切片元素的个数,使用内置函数 len 获取。

容量cap

数组的容量是其长度,切片的容量是切片的第一个元素到底层数组的最后一个元素的长度。

如果切片操作超出 cap(s) 的上限将导致一个panic 异常,但是超出 len(s) 则是意味着扩展了 slice,新 slice 的长度会变大(请见添加元素 - 自动扩展cap)。

// 数组的容量是其长度
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(cap(arr))  // 8// 切片的容量
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]        // [2 3 4 5]
s2 := s1[3:5]         // [5 6],注意,扩展了
fmt.Println(cap(s1))  // 6
fmt.Println(cap(s2))  // 3
复制代码

操作

添加元素

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]
s3 := append(s2, 10)
s4 := append(s3, 11)
s5 := append(s4, 12)
fmt.Println(s1)    // [2 3 4 5]
fmt.Println(s2)    // [5 6],注意,扩展了s1
fmt.Println(s3)    // [5 6 10]
fmt.Println(s4)    // [5 6 10 11]
fmt.Println(s5)    // [5 6 10 11 12]
fmt.Println(arr)   // [0 1 2 3 4 5 6 10]
复制代码

观察最后的 arr, 长度仍然是 7,说明 s3append 改变了原 arrays4s5append 由于超出了底层数组的容量,实际上 s3s4 不再 viewarr 了,而是 view 了个新的更加长的 array

可见,添加元素时如果超越了 cap,系统会重新分配更大的底层数组。如果原数组不再被使用,会被垃圾回收。

自动扩展 cap:

var s []intfor i :=0; i<10; i++ {s = append(s, i)fmt.Println(len(s), cap(s))
}
fmt.Println(s)// 1 1
// 2 2
// 3 4
// 4 4
// 5 8
// 6 8
// 7 8
// 8 8
// 9 16
// 10 16
// [0 1 2 3 4 5 6 7 8 9]
复制代码

删除元素

删除索引为 3 的元素,也可以使用 copy 实现,见模拟stack

arr := [...]int{0, 1, 2, 3, 4, 5}
s := arr[:]     // [0 1 2 3 4 5]s = append(s[:3], s[4:]...)
fmt.Println(s)  // [0 1 2 4 5]
复制代码

切片拷贝

copy(dst slice, src slice):对第一个参数值进行修改。

两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。copy 函数将返回成功复制的元素的个数,等于两个 slice 中较小的长度,所以我们不用担心覆盖会超出目标 slice 的范围。

s1 := []int{1, 2}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1)  // [4 5]s1 := []int{1, 1, 1, 1}
s2 := []int{4, 5, 6}
copy(s1, s2)
fmt.Println(s1)  // [4 5 6 1]
复制代码

遍历

for-range 结构遍历切片

比较

和数组不同的是,slice 之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。

标准库提供了高度优化的 bytes.Equal 函数来判断两个字节型 slice 是否相等([]byte),但是对于其他类型的 slice,我们必须自己展开每个元素进行比较:

func equal(x, y []string) bool {if len(x) != len(y) {return false}for i := range x {if x[i] != y[i] {return false}}return true
}
复制代码

nil

slice 唯一合法的比较操作是和 nil 比较:

if slice1 == nil { /* ... */ }
复制代码
  • 一个零值的 slice 等于 nil
  • 一个 nil 值的 slice 并没有底层数组
  • 一个 nil 值的 slice 的长度和容量都是 0,但是也有非 nil 值的 slice 的长度和容量也是 0 的,例如 []int{}make([]int, 3)[3:]
  • 一个 nil 值的 slice 的行为和其它任意 0 长度的 slice 一样,所有 Go 语言函数应该同等式对待 nil 值的 slice0 长度的 slice
  • 如果需要测试一个 slice 是否是空的,使用 len(s) == 0 来判断,不要用 s == nil 来判断
var s1 []int
fmt.Println(len(s1), s1 == nil)    // 0 trues2 := []int{}
fmt.Println(len(s2), s2 == nil)    // 0 falses3 := []int(nil)
fmt.Println(len(s3), s3 == nil)    // 0 true
复制代码

函数传参

如果一个函数需要对数组操作,最好把参数声明为切片。当调用函数时,把数组分片,传入切片引用

数组元素和

package mainimport "fmt"func sum(a []int) int {s := 0for i := range a {s += i}return s
}func main() {arr := [...]int{0, 1, 2, 3, 4, 5}fmt.Println(sum(arr[:]))          // 15var s1 []intfmt.Println(s1 == nil, sum(s1))   // true 0s2 := []int{}fmt.Println(s2 == nil, sum(s2))   // false 0
}复制代码

反转数组

package mainimport "fmt"func reverse(s []int) {// for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {//     s[i], s[j] = s[j], s[i]// }for index, i := range s {s[index], s[len(s) - i - 1] = s[len(s) - i - 1], s[index]}
}func main() {arr := [...]int{0, 1, 2, 3, 4, 5}reverse(arr[:])fmt.Println(arr)var s1 []intreverse(s1)fmt.Println(s1)s2 := []int{}reverse(s2)fmt.Println(s2)
}
复制代码

应用技巧

去除空值

举个例子,去除切片中的空值。注意,输入的 slice 和输出的 slice 共享同一底层数组,这有可能修改了原来的数组。这是重用原来的 slice ,节约了内存。

package mainimport ("fmt"
)func nonempty(strings []string) []string {i := 0for _, s := range strings {if s != "" {strings[i] = si++}}return strings[:i]
}func main() {data := []string{"one", "", "three"}fmt.Printf("%q\n", nonempty(data))  // ["one" "three"]fmt.Printf("%q\n", data)            // ["one" "three" "three"],改变原数组
}
复制代码

使用 append 函数实现:

func nonempty2(strings []string) []string {out := strings[:0]  // 注意,重用原 slice 的关键for _, s := range strings {if s != "" {out = append(out, s)}}return out
}
复制代码

模拟栈

上面的 nonempty2 函数是用 slice 模拟一个 stack,最初给定的空 slice 对应一个空的 stack

插入元素(push):

stack = append(stack, v)
复制代码

取出最顶部(slice的最后)的元素:

top := stack[len(stack)-1]
复制代码

通过收缩 stack 弹出栈顶的元素(pop):

stack = stack[:len(stack)-1] // pop
复制代码

删除 slice 中间的某个元素并保存原有的元素顺序:

func remove(slice []int, i int) []int {copy(slice[i:], slice[i+1:])return slice[:len(slice)-1]// 或者// return append(slice[:i], slice[i+1:]...)
}
复制代码

深入理解

appendInt

append 函数对于理解 slice 底层是如何工作的非常重要。下面是第一个版本的 appendInt 函数,专门用于处理 []int 类型的 slice

func appendInt(x []int, y int) []int {var z []intzlen := len(x) + 1if zlen <= cap(x) {// There is room to grow.  Extend the slice.z = x[:zlen]} else {// There is insufficient space.  Allocate a new array.// Grow by doubling, for amortized linear complexity.zcap := zlenif zcap < 2*len(x) {zcap = 2 * len(x)}z = make([]int, zlen, zcap)copy(z, x) // a built-in function; see text}z[len(x)] = yreturn z
}
复制代码
  1. 先检测 slice 底层数组是否有足够的容量来保存新添加的元素
  2. 如果空间足够,直接在原有底层数组之上扩展 slice,将新添加的 y 元素复制到新扩展的空间,并返回 slice。可见,输入的 x 和输出的 z 共享相同的底层数组
  3. 如果没有足够的增长空间,appendInt 函数则会先分配一个足够大的 slice 用于保存新的结果,先将输入的 x 复制到新的空间,然后添加 y 元素。结果 z 和输入的 x 引用的将是不同的底层数组
  4. 为了提高内存使用效率,新分配的数组一般略大于保存 xy 所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。

append

内置的 append 函数可能比 appendInt 的内存扩展策略更复杂,通常我们并不知道 append 调用是否导致了内存的重新分配,所以也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。此时,我们就不能确认在原先的 slice 上的操作是否会影响到新的 slice。在这种情况下,通常是将 append 返回的结果直接赋值给输入的 slice 变量:s = append(s, val)

更新 slice 变量不仅对调用 append 函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用 slice,需要记住尽管底层数组的元素是间接访问的,但是 slice 对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看,slice 并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型:

type IntSlice struct {ptr      *intlen, cap int
}
复制代码

内置的 append 函数可以追加多个元素,甚至追加一个 slice:

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
复制代码

修改 appendInt,实现类似 append 函数的功能:

func appendInt(x []int, y ...int) []int {var z []intzlen := len(x) + len(y)// ...expand z to at least zlen...copy(z[len(x):], y)return z
}
复制代码

参考目录

  • Go语言圣经中文版
  • Go 入门指南

Go 学习笔记(11):切片相关推荐

  1. SpringMVC:学习笔记(11)——依赖注入与@Autowired

    SpringMVC:学习笔记(11)--依赖注入与@Autowired 使用@Autowired 从Spring2.5开始,它引入了一种全新的依赖注入方式,即通过@Autowired注解.这个注解允许 ...

  2. Hadoop学习笔记—11.MapReduce中的排序和分组

    Hadoop学习笔记-11.MapReduce中的排序和分组 一.写在之前的 1.1 回顾Map阶段四大步骤 首先,我们回顾一下在MapReduce中,排序和分组在哪里被执行: 从上图中可以清楚地看出 ...

  3. HALCON 20.11:深度学习笔记(11)---目标检测

    HALCON 20.11:深度学习笔记(11)---目标检测 HALCON 20.11.0.0中,实现了深度学习方法. 本章讲解了如何使用基于深度学习的对象检测. 通过对象检测,我们希望在图像中找到不 ...

  4. 台大李宏毅Machine Learning 2017Fall学习笔记 (11)Convolutional Neural Network

    台大李宏毅Machine Learning 2017Fall学习笔记 (11)Convolutional Neural Network 本博客主要整理自: http://blog.csdn.net/x ...

  5. 华为HCIA-datacom 学习笔记11——AAA原理与配置

    华为HCIA-datacom 学习笔记11--AAA原理与配置 AAA原理与配置 1.AAA概述 认证(authentication):验证用户是否获得访问权,确定哪些用户可以访问网络 授权(auth ...

  6. 点云学习笔记11——VoxelNet算法+代码运行

    点云学习笔记11--VoxelNet算法+代码运行 一.算法分析 摘要 介绍 相关工作 1.2. 贡献 2.VoxelNet 2.1.特征学习网络 2.1.1 特征学习网络 二.代码复现 2.1.环境 ...

  7. 凸优化学习笔记 11:对偶原理 拉格朗日函数

    前面讲了凸优化问题的定义,以及一些常见的凸优化问题类型,这一章就要引入著名的拉格朗日函数和对偶问题了.通过对偶问题,我们可以将一些非凸问题转化为凸优化问题,还可以求出原问题的非平凡下界,这对复杂优化问 ...

  8. Python学习笔记11:函数修饰符

    Python学习笔记11:函数修饰符 Python有很多有趣的特性,其中函数修饰符就是一个. 我们在之前的那个web应用示例中用过如下写法: @web.route('/log') @符号后边的,就是一 ...

  9. MIPS汇编语言学习笔记11:整数减法 (mult方法)

    任务:整数相乘,并输出打印. 代码: .dataintA: .word 5intB: .word 2 .textli $v0, 1lw $a1, intAlw $a2, intBmult $a1, $ ...

  10. 从零写一个具有IOC-AOP-MVC功能的框架---学习笔记---11. MVC功能之http请求处理器的编写---简易框架最后一公里!

    从零写一个具有IOC-AOP-MVC功能的框架-学习笔记 专栏往期文章链接: IOC功能相关章节: 从零写一个具有IOC-AOP-MVC功能的框架-学习笔记-01.项目初始化 从零写一个具有IOC-A ...

最新文章

  1. A Tutorial on Clustering Algorithms-聚类小知识
  2. 群星巨型计算机事件,群星 三种特殊事件介绍 特殊事件有几种
  3. 严格地说来的zhajinhuagame
  4. 安卓平板usb变显示器_RJ45与USB信号接入工业显示器有何不同?
  5. java通字乱码_Java解决通信过程的中文乱码的问题
  6. JDBC插入百万数据,不到5秒!
  7. windows 常用网络命令 (cmd)
  8. Android系统webView下载不动,Android WebView下载无法正常工作
  9. 英雄无敌服务器维护,英雄无敌王国服务器地图攻略
  10. MySql超详细分析(InnoDb存储引擎,日志文件,调优,索引,主从复制简单操作等)
  11. 【作业】随机数+参数可变的方法+实验任务(输出素数+使用递归,判断是否为回文+统计一篇英语问斩单词出现频率)...
  12. 对大数据量Excel文件自动排版、转换成PDF用于印刷出版
  13. win10计算机用户名修改密码,win10怎么修改登录用户名 win10修改开机密码的详细教程...
  14. 大数据可视化坐标轴的定制与绘制3D图表及统计地图
  15. 司马谈《论六家要旨》品读
  16. Linux上监控应用程序启动 (hook execve系统调用)
  17. 深度学习6---案例:人民币识别实现
  18. MCU踩坑记录:SWD复用为GPIO使用,Jflash连不上的问题
  19. 驱动lvds双8位时钟_烧写液晶屏驱动软件识别
  20. 队列列列列!!!!! HRBUST - 1940

热门文章

  1. linux 32bit 改为64bit问题
  2. linux系统调用sysconf(获取系统信息)
  3. Glances:一款功能强大的操作系统安全监控平台
  4. paros web中间件攻击、扫描、监控
  5. 非常详细Redis数据库入门教程
  6. linux下c/c++实例之socket服务器实例
  7. 【repost】Javascript操作DOM常用API总结
  8. C#开发微信门户及应用(32)--微信支付接入和API封装使用
  9. 虽然我们可能不想对元素应用3D变换,可我们一样可以开启3D引擎
  10. csdn的blog后台程序的导航菜单的实现