golang之数组切片的具体用法
作者:假装懂编程 发布时间:2024-04-29 13:06:43
数组
go开发者在日常的工作中slice算是用的比较多的了,在介绍slice之前,我们先了解下数组,数组相信大家都不陌生,数组的数据结构比较简单,它在内存中是连续的。以一个存了10个数字的数组为例来说:
a:=[10]int{0,1,2,3,4,5,6,7,8,9}
它在内存中大概是这样的:
得益于连续性,所以数组的特点就是:
大小固定
访问快,复杂度为O(1);
插入和删除元素因为要移动元素,所以相比查询会慢。 当我们要访问一个越界的元素的元素时,go甚至编辑都不通过:
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(a[10])
// invalid array index 10 (out of bounds for 10-element array)
切片
相比数组,go的slice(切片)要相对灵活些,比较大的不同点就是slice的长度可以不固定,创建的时候不用指明长度,在go中slice是一种设计过的数据结构:
type slice struct {
array unsafe.Pointer //指针
len int //长度
cap int //容量
}
slice的底层其实还是数组,通过指针指向它底层的数组,len是slice的长度,cap是slice的容量,slice添加元素时,且cap容量不足时,会根据策略扩容。
切片的创建
直接声明
var s []int
通过直接声明的slice,它是个nil
slice,它的长度和容量都是0,且不指向任何底层数组,nil切片和空切片是不一样的,接下来会介绍。
new方式初始化
s:=*new([]int)
new的方式和直接声明的方式区别不大,最终产出的都是一个nil的slice。
字面量
s1 := []int{0, 1, 2}
s2 := []int{0, 1, 2, 4: 4}
s3 := []int{0, 1, 2, 4: 4, 5, 6, 9: 9}
fmt.Println(s1, len(s1), cap(s1)) //[0 1 2] 3 3
fmt.Println(s2, len(s2), cap(s2)) //[0 1 2 0 4] 5 5
fmt.Println(s3, len(s3), cap(s3)) //[0 1 2 0 4 5 6 0 0 9] 10 10
字面量创建的slice,默认长度和容量是相等的,需要注意的是如果我们单独指明了某个索引的值,那么在这个索引值前面的元素如果未声明的话,就会是slice的类型的默认值。
make方式
s := make([]int, 5, 6)
fmt.Println(s, len(s), cap(s)) //[0 0 0 0 0] 5 6
通过make可以指定slice的长度和容量。
截取方式
切片可以从数组或者其他切片中截取获得,这时新的切片会和老的数组或切片共享一个底层数组,不管谁修改了数据,都会影响到底层的数组,但是如果新的切片发生了扩容,那么底层的数组就不是同一个。
s[:]
a := []int{0, 1, 2, 3, 4}
b := a[:]
fmt.Println(b, len(b), cap(b)) //[0 1 2 3 4] 5 5
通过:
获取 [0,len(a)-1]
的切片,等同于整个切片的引用。
s[i:]
a := []int{0, 1, 2, 3, 4}
b := a[1:]
fmt.Println(b, len(b), cap(b)) //[1 2 3 4] 4 4
通过指定切片的开始位置来获取切片,它是左闭的包含左边的元素,此时它的容量cap(b)=cap(a)-i
。这里要注意界限问题,a[5:]
的话,相当于走到数组的尾巴处,什么元素也没了,此时就是个空切片,但是如果你用a[6:]
的话,那么就会报错,超出了数组的界限。
a := []int{0, 1, 2, 3, 4}
b := a[5:] //[]
c := a[6:] //runtime error: slice bounds out of range [6:5]
c虽然报错了,但是它只是运行时报错,编译还是能通过的。
s[:j]
a := []int{0, 1, 2, 3, 4}
b := a[:4]
fmt.Println(b, len(b), cap(b)) //[0 1 2 3] 4 5
获取[0-j)
的数据,注意右边是开区间,不包含j,同时它的cap和j没关系,始终是cap(b) = cap(a)
,同样注意不要越界。
s[i:j]
a := []int{0, 1, 2, 3, 4}
b := a[2:4]
fmt.Println(b, len(b), cap(b)) //[2 3] 2 3
获取[i-j)
的数据,注意右边是开区间,不包含j,它的cap(b) = cap(a)-i
。
s[i:j:x]
a := []int{0, 1, 2, 3, 4}
b := a[1:2:3]
fmt.Println(b, len(b), cap(b)) //[1] 1 2
通过上面的例子,我们可以发现切片b的cap其实和j没什么关系,和i存在关联,不管j是什么,始终是cap(b)=cap(a)-i
,x
的出现可以修改b的容量,当我们设置x后,cap(b) = x-i
而不再是cap(a)-i
了。
看个例子
s0 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s0[3:6] //[3 4 5] 3 7
s1是对s0的切片,所以它们大概是这样:
s2 := s1[1:3:4]
这时指定个s2,s2是对s1的切片,并且s2的len=2,cap=3,所以大概长这样:
s1[1] = 40
fmt.Println(s0, s1, s2)// [0 1 2 3 40 5 6 7 8 9] [3 40 5] [40 5]
这时把s1[1]修改成40,因为没有涉及到扩容,s0、s1、s2重叠部分都指向同一个底层数组,所以最终发现s0、s2对应的位置都变成了40。
s2 = append(s2, 10)
fmt.Println(s2, len(s2), cap(s2)) //[40 5 10] 3 3
再向s2中添加一个元素,因为s2还有一个空间,所以不用发生扩容。
s2 = append(s2, 11)
fmt.Println(s2, len(s2), cap(s2)) //[40 5 10 11] 4 6
继续向s2中添加一个元素,此时s2已经没有空间了,所以会触发扩容,扩容后指向一个新的底层数据,和原来的底层数组解耦了。
此时无论怎么修改s2都不会影响到s1和s2。
切片的扩容
slice的扩容主要通过growslice
函数上来处理的:
func growslice(et *_type, old slice, cap int) slice {
....
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
....
return slice{p, old.len, newcap}
}
入参说明下:
et
是slice的类型。old
是老的slice。cap
是扩容后的最低容量,比如原来是4,append加了一个,那么cap就是5。 所以上面的代码解释为:如果扩容后的最低容量大于老的slice的容量的2倍,那么新的容量等于扩容后的最低容量。
如果老的slice的长度小于1024,那么新的容量就是老的slice的容量的2倍
如果老的slice的长度大于等于1024,那么新的容量就等于老的容量不停的1.25倍,直至大于扩容后的最低容量。 这里需要说明下关于slice的扩容网上很多文章都说小于1024翻倍扩容,大于1024每次1.25倍扩容,其实就是基于这段代码,但其实这不全对,我们来看个例子:
a := []int{1, 2}
fmt.Println(len(a), cap(a)) //2 2
a = append(a, 2, 3, 4)
fmt.Println(len(a), cap(a)) // 5 6
按照规则1,这时的cap应该是5,结果是6。
a := make([]int, 1280, 1280)
fmt.Println(len(a), cap(a)) //1280 1280
a = append(a, 1)
fmt.Println(len(a), cap(a), 1280*1.25) //1281 1696 1600
按照规则3,这时的cap应该是原来的1.25倍,即1600,结果是1696。
内存对齐
其实上面两个扩容,只能说不是最终的结果,go还会做一些内存对齐的优化,通过内存对齐可以提升读取的效率。
// 内存对齐
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
空切片和nil切片
空切片:slice的指针不为空,len和cap都是0
nil切片:slice的指针不指向任何地址即array=0,len和cap都是0
nil | 空 |
---|---|
var a []int | a:=make([]int,0) |
a:=*new([]int) | a:=[]int{} |
空切片虽然地址不为空,但是这个地址也不代表任何底层数组的地址,空切片在初始化的时候会指向一个叫做zerobase
的地址,
var zerobase uintptr
if size == 0 {
return unsafe.Pointer(&zerobase)
}
所有空切片的地址都是一样的。
var a1 []int
a2:=*new([]int)
a3:=make([]int,0)
a4:=[]int{}
fmt.Println(*(*[3]int)(unsafe.Pointer(&a1))) //[0 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a2))) //[0 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a3))) //[824634101440 0 0]
fmt.Println(*(*[3]int)(unsafe.Pointer(&a4))) //[824634101440 0 0]
数组是值传递,切片是引用传递?
func main() {
array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
changeArray(array)
fmt.Println(array) //[0 1 2 3 4 5 6 7 8 9]
changeSlice(slice)
fmt.Println(slice) //[1 1 2 3 4 5 6 7 8 9]
}
func changeArray(a [10]int) {
a[0] = 1
}
func changeSlice(a []int) {
a[0] = 1
}
定义一个数组和一个切片
通过changeArray改变数组下标为0的值
通过changeSlice改变切片下标为0的值
原数组值未被修改,原切片的值已经被修改 这个表象看起来像是slice是指针传递似的,但是如果我们这样呢:
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
changeSlice(slice)//[0 1 2 3 4 5 6 7 8 9]
}
func changeSlice(a []int) {
a = append(a, 99)
}
会发现原slice的值并没有被改变,这是因为我们用了append,append之后,原slice的容量已经不够了,这时候会copy出一个新的数组。其实go的函数参数传递,只有值传递,没有引用传递,当slice的底层数据没有改变的时候,怎么修改都会影响原底层数组,当slice发生扩容时,扩容后就是新的数组,那么怎么修改这个新的数组都不会影响原来的数组。
数组和slice能不能比较
只有长度相同,类型也相同的数组才能比较
a:=[2]int{1,2}
b:=[2]int{1,2}
fmt.Println(a==b) true
a:=[2]int{1,2}
b:=[3]int{1,2,3}
fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [3]int)
a:=[2]int{1,2}
b:=[2]int8{1,2}
fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [2]int8)
slice只能和nil做比较,其余的都不能比较
a:=[]int{1,2}
b:=[]int{1,2}
fmt.Println(a==b)//invalid operation: a == b (slice can only be compared to nil)
但是需要注意的是,两个都是nil的slice也不能进行比较,它只能和nil对比,这里的nil是真真实实的nil。
var a []int
var b []int
fmt.Println(a == b) //invalid operation: a == b (slice can only be compared to nil)
fmt.Println(a == nil) //true
来源:https://juejin.cn/post/7121628307040403487


猜你喜欢
- 1. dbm UNIX键-值数据库dbm是面向DBM数据库的一个前端,DBM数据库使用简单的字符串值作为键来访问包含字符串的记录。dbm使用
- mysqlcheck客户端工具可以检查和修复MyISAM表,还可以优化和分析表。实际上,它集成了mysql工具中check、repair、a
- whoosh的官方介绍:http://whoosh.readthedocs.io/en/latest/quickstart.html因为做的
- <?php /* *@author 夜无眠  
- 一、实现过程本文对经典手写数字数据集进行多分类,损失函数采用交叉熵,激活函数采用ReLU,优化器采用带有动量的mini-batchSGD算法
- sae中安装有python环境,想让sae导入自己下载的django或者其他模块,可以在svn中新建一个文件目录,比如site-packag
- 今天将webserice里面的一个代码,拷到一个C#类,结果运行编译错误。DataBase = Server.MapPath("d
- 什么是WaitGroupsWaitGroups是同步你的goroutines的一种有效方式。想象一下,你和你的家人一起驾车旅行。你的父亲在一
- shapefile转换geojsonimport shapefileimport codecsfrom json import dumps#
- 姿态检测是计算机视觉领域的一个活跃研究领域。你可以从字面上找到数百篇研究论文和几个试图解决姿势检测问题的模型。之所以有如此多的机器学习爱好者
- 今天看视频学习时学习了一种新技术,即平时我们在一个页面点击“提交”或“确认”会自动跳转到一个页面。 在网上搜了一下,关于这个技术处理有多种方
- 一、条件控制Python条件语句是通过一条或多条语句的执行结果(True或者False)来决定执行的代码块,而计算机很多自动化任务,也是根据
- 2021年7月1日,官方正式发布了1.0Datatable版本。1.0版本支持windows和linux,以及Macos。 具体文档可以见:
- 获取计算机名# 获取计算机名,常用的方法有三种,但最常用的是第一种import osimport socket# method onenam
- 本文介绍MySQL数据库中执行select查询语句,查询指定列的数据,即指定字段的数据。再来回顾一下SQL语句中的select语句的语法:S
- 超级鹰平台验证码的破解可以有以下方式:简单的数字字母组合可以使用图像识别(python 现成模块),成功率不高使用第三方打码平台(破解验证码
- 项目要求:读完题目,首先我们要确定程序思路我们要全部通过类去实现也就是 我们要实现管理员、学生、讲师、课程、教师五个类管理员类class A
- vue切换“页面”(路由)时保持滚动条回到顶部vue项目做pc端的时候,发现在两个页面切换时 滚动条
- 同样是取10条数据 select * from yanxue8_visit limit 10000,10 和 select * from y
- 主要内容一.网络存储是什么?二.iSCSI是什么?三.RDMA是什么?四.NVME-oF是什么?一.网络存储是什么?网络存储是一种将存储资源