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处理器系列 ...
随机推荐
- ElementUI常遇到的一些问题
一.form 下面只有一个 input 时回车键刷新页面 原因是:触发了表单默认的提交行为,给el-form 加上 @submit.native.prevent 就行了. <el-form in ...
- shiro 快速入门详解。
package com.aaa.lee.shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; i ...
- VC++线程同步之临界区(CriticalSection)
1.相关文件和接口 #include <windows.h> CRITICAL_SECTION cs;//定义临界区对象 InitializeCriticalSection(&cs ...
- SpringBoot集成druid数据库连接池的简单使用
简介 Druid是阿里巴巴旗下Java语言中最好的数据库连接池.Druid能够提供强大的监控和扩展功能. 官网: https://github.com/alibaba/druid/wiki/常见问题 ...
- JS let, var, const的用法以及区别
本文摘自多位前辈的博文,另外还有一些我的多余补充,摘自地址已补充.非常感谢各位前辈.仅以笔记学习为目的! 深入学习ES6的知识还请访问阮一峰老师的ES6教程 如果不使用let或者const,在JS只有 ...
- java篇之JDBC原理和使用方法
JDBC学过但又属于很容易忘记的那种,每次要用到,都要看下连接模式.每次找又很费时间,总之好麻烦呀呀呀,所以写篇博客,总结下原理和常用接口,要是又忘了可以直接来博客上看,嘿嘿. 一.什么是JDBC 1 ...
- 反射(reflection),通过反射创建对象
简单尝试: import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public cl ...
- php异步:在php中使用fsockopen curl实现类似异步处理的功能方法
PHP从主流来看,是一门面向过程的语言,它的最大缺点就是无法实现多线程管理,其程序的执行都是从头到尾,按照逻辑一路执行下来,不可能出现分支,这一点是限制php在主流程序语言中往更高级的语言发展的原因之 ...
- Java基于ClassLoder/ InputStream 配合读取配置文件
阅读java开源框架源码或者自己开发系统时配置文件是一个不能忽略的,在阅读开源代码的过程中尝尝困惑配置文件是如何被读取到内存中的.配置文件本身只是为系统运行提供参数的支持,个人阅读源码时重点不大可能放 ...
- CentOS8上安装MySQL
没有选择Win10上安装MySQL,个人感觉比较傻瓜式.同时相对Win10操作系统,个人更熟悉Unix/Linux操作系统,所以选择在CentOS8上安装MySQL数据库. 还是熟悉的yum安装,前提 ...