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。在第二阶段中,元素类型大小 intsys.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是一个副本,改变的是副本的lenouterSlicelen实际并没有变化,所以输出的值会比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切片全解析的更多相关文章

  1. Python 最常见的 170 道面试题全解析:2019 版

    Python 最常见的 170 道面试题全解析:2019 版 引言 最近在刷面试题,所以需要看大量的 Python 相关的面试题,从大量的题目中总结了很多的知识,同时也对一些题目进行拓展了,但是在看了 ...

  2. Google Maps地图投影全解析(3):WKT形式表示

    update20090601:EPSG对该投影的编号设定为EPSG:3857,对应的WKT也发生了变化,下文不再修改,相对来说格式都是那样,可以到http://www.epsg-registry.or ...

  3. C#系统缓存全解析(转载)

    C#系统缓存全解析 对各种缓存的应用场景和方法做了很详尽的解读,这里推荐一下 转载地址:http://blog.csdn.net/wyxhd2008/article/details/8076105

  4. 【凯子哥带你学Framework】Activity界面显示全解析

    前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分 ...

  5. iOS Storyboard全解析

    来源:http://iaiai.iteye.com/blog/1493956 Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果, ...

  6. 【转载】Fragment 全解析(1):那些年踩过的坑

    http://www.jianshu.com/p/d9143a92ad94 Fragment系列文章:1.Fragment全解析系列(一):那些年踩过的坑2.Fragment全解析系列(二):正确的使 ...

  7. (转)ASP.NET缓存全解析6:数据库缓存依赖

    ASP.NET缓存全解析文章索引 ASP.NET缓存全解析1:缓存的概述 ASP.NET缓存全解析2:页面输出缓存 ASP.NET缓存全解析3:页面局部缓存 ASP.NET缓存全解析4:应用程序数据缓 ...

  8. jQuery&nbsp;Ajax&nbsp;实例&nbsp;全解析

    jQuery Ajax 实例 全解析 jQuery确实是一个挺好的轻量级的JS框架,能帮助我们快速的开发JS应用,并在一定程度上改变了我们写JavaScript代码的习惯. 废话少说,直接进入正题,我 ...

  9. 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处理器系列 ...

随机推荐

  1. 009 Linux 文件大小统计与排序( du于df和sort)

    @ 目录 01 du 与 df 作用与区别? du(disk usage) df(disk free) 02 du 常用命令示例 03 sort 常用参数 04 常用组合 du + sort + he ...

  2. SuperPoint: Self-Supervised Interest Point Detection and Description 论文笔记

    Introduction 这篇文章设计了一种自监督网络框架,能够同时提取特征点的位置以及描述子.相比于patch-based方法,本文提出的算法能够在原始图像提取到像素级精度的特征点的位置及其描述子. ...

  3. [论文][半监督语义分割]Adversarial Learning for Semi-Supervised Semantic Segmentation

    Adversarial Learning for Semi-Supervised Semantic Segmentation 论文原文 摘要 创新点:我们提出了一种使用对抗网络进行半监督语义分割的方法 ...

  4. 马哈鱼间接数据流中的where-group-by子句

    马哈鱼间接数据流中的where-group-by子句 本文介绍间接数据流中的where-group-by子句. 1.列在where子句中 WHERE子句中源表中的某些列不影响目标列,但对所选行集至关重 ...

  5. nested exception is java.lang.NoClassDefFoundError: org/fusesource/hawtbuf/UTF8Buffer

    前言:IDE管理maven项目,总是遇到各种莫名奇妙的问题,有的是导入了依赖,IDE确报包未找到,有的是IDE显示找到,但是控制台确报未找到,有以下几种方法可以解决 第一:确认自己导入的依赖是否有问题 ...

  6. Java Calendar类的使用总结【转】

    感谢!原文地址:https://www.cnblogs.com/huangminwen/p/6041168.html Java Calendar类的使用总结 在实际项目当中,我们经常会涉及到对时间的处 ...

  7. iOS中利用CoreTelephony获取用户当前网络状态(判断2G,3G,4G) by徐文棋

    前言: 在项目开发当中,往往需要利用网络.而用户的网络环境也需要我们开发者去注意,根据不同的网络状态作相应的优化,以提升用户体验. 但通常我们只会判断用户是在WIFI还是移动数据,而实际上,移动数据也 ...

  8. MySQL基本命令操作及数据库基本概念

    MySQL基本命令操作及数据库基本概念 1.数据库的基本概念 2.主流数据库介绍 3.关系型非关系型数据库介绍 4.Mysql 安装方法 5.Mysql 基本操作命令 1.数据库的基本概念: 数据库的 ...

  9. Nginx+Tomcat负载均衡、动静分离群集

    Nginx+Tomcat负载均衡.动静分离群集 目录 Nginx+Tomcat负载均衡.动静分离群集 一.Tomcat 1. Tomcat简介 2. Tomcat重要目录 二.Nginx负载均衡原理 ...

  10. 范数||x||(norm)笔记

    1. 范数的含义和定义 范数是具有"长度"概念的函数.在线性代数.泛函分析及相关领域,是一个函数,它为向量空间内的所有向量赋予非零的正的长度或大小.另一方面,半范数可以为非零的向量 ...