Go切片全解析
Go切片全解析
目录结构:
数组
切片
- 底层结构
- 创建
- 普通声明
- make方式
- 截取
- 边界问题
- 追加
- 拓展表达式
- 扩容机制
- 切片传递的坑
- 切片的拷贝
- 浅拷贝
- 深拷贝
数组
var n [4]int
fmt.Println(n) //输出:[0 0 0 0]
n[0] = 1
n[3] = 2
fmt.Println(len(n)) //输出: 4
fmt.Println(cap(n)) //输出:4
fmt.Println(n) //输出:[1 0 0 2]
b := n
n[0] = 2
fmt.Println(b) //输出: [1 0 0 2]
b[0] = 3
fmt.Println(n) //输出: [2 0 0 2]
说明:
- 在
var n [4]int
就已经完成了数组的初始化,并且全部赋值为0,长度和容量都为4 - 把n赋值给b,相当于对n进行
copy
操作,再把copy
后的结果赋值给b,所以n和b是分别属于两个数组,互不影响
切片
底层结构
type slice struct {
array unsafe.Pointer // 指针指向底层数组
len int // 切片长度
cap int // 底层数组容量
}
创建
声明方式
默认值是nil,初始的长度和容量都为0
var s []int
fmt.Println(cap(s)) // 0
fmt.Println(len(s)) // 0
fmt.Println(s == nil) // true
make创建
make([]interface{}, len, cap)
通过make
创建,默认值不为nil,且初始的长度和容量都可指定,不受自动扩容机制干扰,并且当初始的len
参数不为0时,会像数组那样自动赋值
a := make([]int, 0, 10)
fmt.Println(len(a)) // 0
fmt.Println(cap(a)) // 10
fmt.Println(a == nil) // false
b := make([]int, 1000)
fmt.Println(len(b)) // 1000
fmt.Println(cap(b)) // 1000
c := make([]int, 5, 10)
fmt.Println(len(c)) // 5
fmt.Println(cap(c)) // 10
fmt.Println(c) // [0 0 0 0 0]
截取
切片可以基于数组和切片来创建,截取的规则是左闭右开
n := [5]int{1, 2, 3, 4, 5}
n1 := n[1:] // 从n数组中截取
fmt.Println(n1) // [2 3 4 5]
n2 := n1[1:] // 从n1切片中截取
fmt.Println(n2) // [3 4 5]
切片与原数组或切片是共享底层空间的,接着上面例子,把n2的元素修改之后,会影响原切片和数组:
n2[1] = 6 // 修改n2,会影响原切片和数组
fmt.Println(n1) // [2 3 6 5]
fmt.Println(n2) // [3 6 5]
fmt.Println(n) // [1 2 3 6 5]
边界问题
- 1、当n为数组或字符串表达式
n[low:high]
中low和high的取值关系:
0 <= low <=high <= len(n)
- 2、当n为切片的时候,表达式
n[low:high]
中high最大值变成了cap(n),low和high的取值关系:
0 <= low <=high <= cap(n)
不满足以上条件会发送越界panic。
不同点,有边界数组是len(n)
,切片是cap(n)
追加
内置函数append()
用于向切片中追加元素。
n := make([]int, 0)
n = append(n, 1) // 添加一个元素
n = append(n, 2, 3, 4) // 添加多个元素
n = append(n, []int{5, 6, 7}...) // 添加一个切片
fmt.Println(n) // [1 2 3 4 5 6 7]
当append操作的时候,切片容量如果不够,会触发扩容,接着上面的例子:
fmt.Println(cap(n)) // 容量等于8
n = append(n, []int{8, 9, 10}...)
fmt.Println(cap(n)) // 容量等于16,发生了扩容
当一开始容量是8,后面追加了切片[]int{8, 9, 10}之后,容量变成了16。
如果append超过切片的长度会重新生产一个全新的切片,不会覆盖原来的:
n2 := n[1:4:5] // 长度等于3,容量等于4
fmt.Printf("%p\n", n2) // 0xc0000ac068
n2 = append(n2, 5)
fmt.Printf("%p\n", n2) // 0xc0000ac068
n2 = append(n2, 6)
fmt.Printf("%p\n", n2) // 地址发生改变,0xc0000b8000
拓展表达式
简单表达式生产的新切片与原数组或切片会共享底层数组,虽然避免了copy,但是会带来一定的风险。下面这个例子当新的n1切片append添加元素的时候,覆盖了原来n的索引位置4的值,导致你的程序可能是非预期的,从而产生不良的后果
n := []int{1, 2, 3, 4, 5, 6}
n1 := n[1:4]
fmt.Println(n) // [1 2 3 4 5 6]
fmt.Println(n1) // [2 3 4]
n1 = append(n1, 100) // 把n的索引位置4的值从原来的5变成了100
fmt.Println(n) // [1 2 3 4 100 6]
fmt.Println(n1) // [2 3 4 100]
fmt.Println(len(n[1:4])) // 3
fmt.Println(cap(n[1:4])) // 5
关于容量
n[1:4]的长度是3好理解(4-1),容量为什么是5?
因为切片n[1:4]和切片n是共享底层空间,所以它的容量并不等于他的长度3,根据1等于索引1的位置(等于值2),从值2这个元素开始到末尾元素6,共5个,所以n[1:4]
容量是5。
Go 1.2[3]中提供了一种可以限制新切片容量的表达式:
n[low:high:max]
max表示新生成切片的容量,新切片容量等于max-low
,表达式中low、high、max关系:
0 <= low <= high <= max <= cap(n)
继续刚才的例子,会用max的值来重新计算容量,而不是共享n的容量,但是n2和n还是共享同一个底层数组
n2 := n[1:4:5]
fmt.Println(cap(n2)) // 4
fmt.Println(n2) // 输出 [2 3 4]
n[3] = 111
fmt.Println(n2) // 输出 [2 3 111]
扩容机制
关于Go切片的扩容机制,网上文章很多,很多结论是这样的:
结论1:
- 1、当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
- 2、当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
结论2:
- 在结论1的基础上(切片的预估容量阶段),提到了
内存对齐
,容量计算完了后还要考虑到内存的高效利用,进行内存对齐。
例子
package main
func main() {
s := []int{1,2}
s = append(s, 3,4,5)
println(cap(s)) //输出6
}
由于初始 s
的容量是2,现需要追加3个元素,所以通过 append
一定会触发扩容,并调用 growslice
函数,此时他的入参 cap
大小为2+3=5。通过翻倍原有容量得到 doublecap
= 2+2,doublecap
小于 cap
值,所以在第一阶段计算出的期望容量值 newcap=5
。在第二阶段中,元素类型大小 int
和 sys.PtrSize
相等,通过 roundupsize
向上取整内存的大小到 capmem
= 48 字节,所以新切片的容量newcap
为 48 / 8 = 6 ,成功解释!
在切片 append
操作时,如果底层数组已无可容纳追加元素的空间,则需扩容。扩容并不是在原有底层数组的基础上增加内存空间,而是新分配一块内存空间作为切片的底层数组,并将原有数据和追加数据拷贝至新的内存空间中。
在扩容的容量确定上,相对比较复杂,它与CPU位数、元素大小、是否包含指针、追加个数等都有关系。当我们看完扩容源码逻辑后,发现去纠结它的扩容确切值并没什么必要。
在实际使用中,如果能够确定切片的容量范围,比较合适的做法是:切片初始化时就分配足够的容量空间,在append追加操作时,就不用再考虑扩容带来的性能损耗问题。
切片传递的坑
例子1
有以下例子
func modifySlice(innerSlice []string) {
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 输出如下
[b b]
[b b]
在上面的例子中,切片内容都得到了修改。
例子2
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := []string{"a", "a"}
modifySlice(outerSlice)
fmt.Print(outerSlice)
}
// 输出如下
[b b a]
[a a]
说明:
- 在modifySlice方法中,innerSlice是outerSlice的副本,但是共同引用相同的底层数组,所以在例子1中,切片内容都得到了修改。
- innerSlice是一个len和cap都相同的切片,当append方法发生时,会进行扩容操作,扩容操作会使得产生一个新的切片,是在原有的数组中进行深拷贝,并且扩大容量。
对代码的细节进行打印再次看一下输出结果
func modifySlice(innerSlice []string) {
fmt.Println("begin modify")
innerSlice = append(innerSlice, "a")
fmt.Printf("%p, %v\n", innerSlice, &innerSlice[0])
fmt.Println("innerSlice len:", len(innerSlice), "cap:", cap(innerSlice))
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
fmt.Println("end modify")
}
func main() {
outerSlice := []string{"a", "a"}
fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
fmt.Println("outerSlice len:", len(outerSlice), "cap:", cap(outerSlice))
modifySlice(outerSlice)
fmt.Println("outerSlice len:", len(outerSlice), "cap:", cap(outerSlice))
fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
fmt.Print(outerSlice)
}
//输出
0xc0000464e0, 0xc0000464e0
outerSlice len: 2 cap: 2
begin modify
0xc000022240, 0xc000022240 //地址转换
innerSlice len: 3 cap: 4 //容量改变
[b b a]
end modify
outerSlice len: 2 cap: 2
0xc0000464e0, 0xc0000464e0
[a a]
证明了我们的猜想。
例子3
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
fmt.Println(innerSlice)
}
func main() {
outerSlice := make([]string, 0, 3)
outerSlice = append(outerSlice, "a", "a")
modifySlice(outerSlice)
fmt.Println(outerSlice)
}
//输出
[b b a]
[b b]
说明:
- 初始化切片的容量为3,所以在
innerSlice
不会发生扩容操作,但是由于是值传递,innerSlice
只是outerSlice
的一个副本,当进行append操作的时候,也是对同一个数组进行插入,同时改变innerSlice
的长度,但是outerSlice
的长度(len
字段)并没有发生改变,所以打印出来的还是[b b]
补充一下打印的细节并稍微做点处理
func modifySlice(innerSlice []string) {
innerSlice = append(innerSlice, "a")
innerSlice[0] = "b"
innerSlice[1] = "b"
for { //不断打印OuterSlice的内存地址以及值
time.Sleep(time.Second / 10)
fmt.Printf("%p\n", innerSlice)
fmt.Println(innerSlice)
}
}
func main() {
outerSlice := make([]string, 0, 3) //初始化容量为3长度为0的切片
outerSlice = append(outerSlice, "a", "a")
fmt.Printf("outerSlice %p\n", outerSlice) //打印innerSlice初始的内存地址
go modifySlice(outerSlice) //执行modifySlice
time.Sleep(time.Second / 5) //等待modifySlice结束
fmt.Println(outerSlice) //再次打印innerSlice的值
fmt.Println("outerSlice", len(outerSlice), cap(outerSlice)) //打印innerSlice的长度和容量
outerSlice = append(outerSlice, "b")
fmt.Println(outerSlice) ////再次打印innerSlice的值
fmt.Printf("outerSlice %p\n", outerSlice) //再次打印innerSlice的内存地址
time.Sleep(time.Second) //等待modify方法的输出
}
//输出
outerSlice 0xc0000c4c60 //outerSlice的初始的内存地址
0xc0000c4c60 //innerSlice的内存地址
[b b a] //modify后的值
[b b]
outerSlice 2 3
[b b b]
outerSlice 0xc0000c4c60 //outerSlice的内存地址没有发生改变
0xc0000c4c60 //innerSlice的内存地址的值没有发生改变
[b b b] //innerSlice的值被覆盖了
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
由此可以说明,当append()执行的时候,没有进行扩容的话还是共享同一个数组,但因为是值传递,innerSlice
是一个副本,改变的是副本的len
,outerSlice
的len
实际并没有变化,所以输出的值会比innerSlice
少
切片的拷贝
浅拷贝
通过=
操作符拷贝切片,这是浅拷贝。
func main() {
a := []int{1, 2, 3}
b := a
fmt.Println(unsafe.Pointer(&a)) // 0xc00000c030
fmt.Println(a, &a[0]) // [100 2 3] 0xc00001a078
fmt.Println(unsafe.Pointer(&b)) // 0xc00000c048
fmt.Println(b, &b[0]) // [100 2 3] 0xc00001a078
}
通过[:]
方式复制切片,同样是浅拷贝。
func main() {
a := []int{1, 2, 3}
b := a[:]
fmt.Println(unsafe.Pointer(&a)) *// 0xc0000a4018*
fmt.Println(a, &a[0]) *// [1 2 3] 0xc0000b4000*
fmt.Println(unsafe.Pointer(&b)) *// 0xc0000a4030*
fmt.Println(b, &b[0]) *// [1 2 3] 0xc0000b4000*
}
深拷贝
深拷贝,需要用到copy()
内置函数
func copy(dst, src []Type) int
其返回值代表切片中被拷贝的元素个数
func main() {
a := []int{1, 2, 3}
b := make([]int, len(a), len(a))
copy(b, a)
fmt.Println(unsafe.Pointer(&a)) *// 0xc00000c030*
fmt.Println(a, &a[0]) *// [1 2 3] 0xc00001a078*
fmt.Println(unsafe.Pointer(&b)) *// 0xc00000c048*
fmt.Println(b, &b[0]) *// [1 2 3] 0xc00001a090*
}
copy 的元素数量与原始切片和目标切片的大小、容量有关系,并且只是往原有的切片进行数据替换,不会产生新的切片
func main() {
a := []int{1, 2, 3}
b := []int{-1, -2, -3, -4}
c := []int{-1, -2}
fmt.Println(unsafe.Pointer(&b)) //0xc0000040f0
copy(b, a)
fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
fmt.Println(a, &a[0]) // [1 2 3] 0xc0000145e8
fmt.Println(unsafe.Pointer(&b)) // 0xc0000040f0
fmt.Println(b, &b[0]) // [1 2 3 -4] 0xc0000101e0
fmt.Println(unsafe.Pointer(&c)) //0xc000004108
copy(c, a)
fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
fmt.Println(a, &a[0]) // [1 2 3] 0xc0000145e8
fmt.Println(unsafe.Pointer(&c)) // 0xc000004108
fmt.Println(c, &c[0]) // [1 2] 0xc0000129a0
}
Go切片全解析的更多相关文章
- Python 最常见的 170 道面试题全解析:2019 版
Python 最常见的 170 道面试题全解析:2019 版 引言 最近在刷面试题,所以需要看大量的 Python 相关的面试题,从大量的题目中总结了很多的知识,同时也对一些题目进行拓展了,但是在看了 ...
- Google Maps地图投影全解析(3):WKT形式表示
update20090601:EPSG对该投影的编号设定为EPSG:3857,对应的WKT也发生了变化,下文不再修改,相对来说格式都是那样,可以到http://www.epsg-registry.or ...
- C#系统缓存全解析(转载)
C#系统缓存全解析 对各种缓存的应用场景和方法做了很详尽的解读,这里推荐一下 转载地址:http://blog.csdn.net/wyxhd2008/article/details/8076105
- 【凯子哥带你学Framework】Activity界面显示全解析
前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分 ...
- iOS Storyboard全解析
来源:http://iaiai.iteye.com/blog/1493956 Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果, ...
- 【转载】Fragment 全解析(1):那些年踩过的坑
http://www.jianshu.com/p/d9143a92ad94 Fragment系列文章:1.Fragment全解析系列(一):那些年踩过的坑2.Fragment全解析系列(二):正确的使 ...
- (转)ASP.NET缓存全解析6:数据库缓存依赖
ASP.NET缓存全解析文章索引 ASP.NET缓存全解析1:缓存的概述 ASP.NET缓存全解析2:页面输出缓存 ASP.NET缓存全解析3:页面局部缓存 ASP.NET缓存全解析4:应用程序数据缓 ...
- jQuery Ajax 实例 全解析
jQuery Ajax 实例 全解析 jQuery确实是一个挺好的轻量级的JS框架,能帮助我们快速的开发JS应用,并在一定程度上改变了我们写JavaScript代码的习惯. 废话少说,直接进入正题,我 ...
- ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57
转自: ARM内核全解析,从ARM7,ARM9到Cortex-A7,A8,A9,A12,A15到Cortex-A53,A57 前不久ARM正式宣布推出新款ARMv8架构的Cortex-A50处理器系列 ...
随机推荐
- Python中open函数怎么操作文件--9
转:https://www.tuicool.com/wx/vYjaYnV 在 Python 中,如果想要操作文件,首先需要创建或者打开指定的文件,并创建一个文件对象,而这些工作可以通过内置的 open ...
- shell循环ping ip的写法
#!/bin/bash for i in `seq 1 20` do if ping -w 2 -c 1 192.168.43.$i | grep "100%" > /dev ...
- How to find out which process is listening upon a port
When we covered port scanning a short while ago we discovered how to tell which ports had processes ...
- How to check in Windows if you are using UEFI
You might be wondering if Windows is using UEFI or the legacy BIOS, it's easy to check. Just fire up ...
- 论文笔记——事件抽取之DMCNN
1.事件抽取介绍: 事件在不同领域中有着不同的含义,对于事件目前还没有统一的定义.在IE ( Information Extraction) 中,事件是指在某个特定的时间片段和地域范围内发生的,由一个 ...
- Redis下载安装与配置(windows)
一.Redis下载 Redis官网建议使用Linux进行部署,未提供windows版本的Redis,但微软开发和维护着Windows64版本的Redis. Windows64版本的Redis下载地址: ...
- Java线程--Callable使用
原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11871727.html Java线程--Callable使用 Callable和Runnabl ...
- checkstyle使用介绍
1.我下载的是checkstyle-5.5-bin.zip:下载地址 http://sourceforge.net/projects/checkstyle/files/ 另一个是checkstyle的 ...
- The official raywenderlich.com Objective-C style guide.
The official raywenderlich.com Objective-C style guide. This style guide outlines the coding convent ...
- LeetCode随缘刷题之两数相加
逐步解释,有说错的地方欢迎指正. package leetcode.day_12_03; /** * 给你两个非空 的链表,表示两个非负的整数.它们每位数字都是按照逆序的方式存储的,并且每个节点只能存 ...