彻底搞懂golang中的数组和切片slice

切片slice是golang中的一种非常重要和关键的数据类型,被大量地使用。本文总结数组arrays的使用,切片slice的使用以及它的底层是如何实现的。在了解底层之后,以后slice的使用上会更胸有成竹。

一、数组arrays

golang中的切片slice其实是数组arrays的一种抽象,所以要搞懂切片slice,就要先弄明白数组arrays。

数组arrays很好理解,就是一个固定长度、固定元素类型的数组。在go中数组类型包含两层意思:长度和元素类型。因此数组[2]int和数组[3]int,这两个是不同类型。虽然元素类型相同,但是长度不同。访问某个元素,可以使用下标的方式,array[n]会访问数组第n个元素,从0开始。

var a [2]int 
var b [3]int 
b[0] = 123
a = b

因为变量a和b是不同类型,所以上面的赋值语句在编译时就报错了:

./main.go:23:4: cannot use b (type [3]int) as type [2]int in assignment
Compilation finished with exit code 2

数组不需要主动的进行初始化,相对应的0值会在声明后被赋值。在内存中[2]int就是线性排列的2个int值,所以数组访问是O(1)的时间复杂度,速度极快。

var a [2]int    // [0 0] 
var c [3]string    // [3个空字符串] 

还有一点要搞清楚,数组arrays在go中是值,而不是指针。不像c或者java,数组是指向底层数组第1个元素的指针。因此在go中你赋值或者传递数组arrays,都会对整个数组内容进行一份复制。所以为了避免无谓的复制,我们会传递数组的指针,而不是数组。

初始化声明一般有两种。e这种声明不用填长度,编译器会帮你计算这个数。

d := [2]int{11, 22} 
e := [...]int{11, 22} 
fmt.Println(d == e)  // 输出 true

二、切片slices

2.1 使用

由于数组arays的特性,在go代码中适用场景有限,而切片slices会用得非常多。切片slices基于数组,但提供了更高的灵活性。

[]T就是一个切片slices,和声明数组的区别就是没有指定长度。可以使用范围来截取切片,例如s1[x:y],会将s1中x位的元素至y-1位的元素截取。

d := [2]int{11, 22} // 数组类型 
s1 := []int{11, 22, 33} // 切片类型 
fmt.Println(s1[:2])  // [11 22]
fmt.Println(s1[1:2]) // [22]

创建一个切片会经常用到内建函数make,它的函数声明如下。我们可以看到,make可接受3个参数,第1个是切片,第2个是切片的长度,第3个是可选的容量大小。不指定cap容量的话,默认会和长度len相同。

func make([]T, len, cap) []T

2.2 slicing的底层细节

s := make([]int, 5),s底层即为上图的数据结构。ptr是一个指针,指向底层对应的数组。len是切片的长度5,cap是底层数组的容量5。

当我们执行下面语句时 :

s2 := s[1:3]

做slicing的时候,go会新建一个slice值s2,而底层的数据是不动的。s2如上图深蓝色,通过更改指针、长度和容量来进行slicing。这也就是为什么slicing的性能非常高的原因。

一个slice不能越过cap进行操作,这个我们从底层很容易理解,因为就相当于越过底层数组的上界进行非法访问了。

2.3 切片增长

一般我们会使用内部函数append来往切片slice后动态追加元素,当cap不够时,如果reslice后可以放下,那么它会reslice。例如上面的s2,底层的数组足以再追加2个元素。如果不行,那么它会new一个新的底层数组,大小为之前cap的两倍,并将之前的元素复制进去。下面测试了下:

s := make([]int, 5) 
s = append(s, 6, 7) 
fmt.Println(len(s), cap(s)) // 输出7 10 
s = append(s, 8, 9, 10, 11) 
fmt.Println(len(s), cap(s))//输出11 20 

如果要追加一个slice到另一个slice的话,这样:

s5 := make([]int, 5) 
s6 := []int{11, 22, 33, 44, 55, 66} 
s := append(s5, s6...) 
fmt.Println(s, len(s), cap(s))//输出[0 0 0 0 0 11 22 33 44 55 66] 11 12 

2.4 注意内存

在这样的场景下注意:如果我们只用到一个slice的一小部分,那么底层的整个数组也将继续保存在内存当中。当这个底层数组很大,或者这样的场景很多时,可能会造成内存急剧增加,造成崩溃。

所以在这样的场景下,我们可以将需要的分片复制到一个新的slice中去,减少内存的占用。例如一个很大的切片data里,我们需要的数据是data[m:n],那么我们创建一个新的slice变量r,将数据复制到r中返回。

mydata := data[m:n]
r := make([]int, len(mydata))
copy(r, mydata) 
return r 

三、总结

通过了解数组array和切片slice的使用和底层原理,可以更透彻的理解他们的使用场景,里面有什么坑。这对我们平时编写程序是极有裨益的,后面我会继续深入,有时间再更。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

【JS游戏编程基础】关于js里的this关键字的理解

this关键字在c++,java中都提供了这个关键字,在刚开始学习时觉得有难度,但是只要理解了,用起来就方便多了,下面通过本篇文章给大家详解js里this关键字...

46410
来自专栏python百例

06-字符串使用基础

942
来自专栏Hongten

python开发_python中的Boolean运算和真假值

1851
来自专栏猿人谷

一个正则表达式测试(只可输入中文、字母和数字)

  在项目中碰到了正则表达式的运用,正则还是非常强大的,不管什么编程语言,基本上都可以用到。之前在用java时特别是对用户名或密码使用正则非常爽,写脚本上用正则...

6296
来自专栏无所事事者爱嘲笑

关于setTimeout和setInterval的函数参数问题

1542
来自专栏前端架构与工程

【译】《Understanding ECMAScript6》- 第一章-基础知识(一)

目录: 更好的Unicode编码支持 codePointAt()函数 String.fromCodePoint() 用转义序列对Non-BMP字符编码 nor...

2645
来自专栏小狼的世界

Javascript设计模式学习(三)更多的高级样式

if (hid != null && hid != undefined & hid != "") {

872
来自专栏Golang语言社区

Go语言的fmt包中文教程

Fmt包 import "fmt" 简介 ▾ Package fmt包含有格式化I/O函数,类似于C语言的printf和scanf。格式字符串的规则来源于C但更...

3997
来自专栏Pythonista

Golang之匿名函数和闭包

 基本概念 闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者 任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块...

2471
来自专栏柠檬先生

你不知道的javaScript笔记(6)

语法   语句表达式       句子是完整表达某个意思的一组词,由一个或多个短语组成,他们之间由标点符号或者连接词连接起来。       语句相当于句子,表达...

2057

扫码关注云+社区