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. 使用Xamarin开发移动应用示例——数独游戏(五)保存游戏进度

    项目代码可以从Github下载:https://github.com/zhenl/ZL.Shudu .代码随项目进度更新. 保存进度是移动应用的基本功能,在应用的使用过程中会有各种各样的可能导致使用中 ...

  2. hashmap 实现 相同的key值时,value值叠加效果。

    一,了解一些基础 package com.ohs.demo; /** * * 一.需求是:停止相同的key值,覆盖效果,将重复的value值,叠加起来. * * 二.hash? 什么是hash? * ...

  3. 理解https中的安全及其实现原理

    Google的一份网络上的 HTTPS 加密透明报告(数据截至2022年1月)中指出HTTPS 连接的普及率在过去几年激增,互联网上排名前 100 位的非 Google 网站HTTPS 使用情况为:9 ...

  4. Redis的最常被问到知识点总结 (转)

    1.什么是redis? Redis 是一个基于内存的高性能key-value数据库. 2.Reids的特点 Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库 ...

  5. httpclient方式提交数据到服务器

    get方式: //使用HttpClient请求服务器将用户密码发送服务器验证 try{ String path = "http://192.168.13.83:8080/xuexi/serv ...

  6. Java基础复习(五)

    1. 接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承具体类(concrete class)? 抽象类中是否可以有静态的main方法? 接口可以继承接口.抽象类可 ...

  7. Solution -「Gym 102956F」Find the XOR

    \(\mathcal{Description}\)   Link.   给定 \(n\) 个点 \(m\) 条边的连通无向图 \(G\),边有边权.其中 \(u,v\) 的距离 \(d(u,v)\) ...

  8. Solution -「CERC 2016」「洛谷 P3684」机棚障碍

    \(\mathcal{Description}\)   Link.   给一个 \(n\times n\) 的网格图,每个点是空格或障碍.\(q\) 次询问,每次给定两个坐标 \((r_1,c_1), ...

  9. Netty高级应用及聊天室实战

    Netty 高级应用 1. 编解码器 概念:在网络应用中,需要实现某种编解码器.将原始字节数据与自定义消息数据进行相互转换.网络中都是以字节码的形式传输的. 对Netty而言,编解码器由两部分组成:编 ...

  10. Unreal ListView使用篇

    应用 ListView,在Unreal UI界面开发中用途非常广泛,基本只要你使用列表,就得需要用ListView.比如排行榜100个列表,界面上只需要显示出10个,我们肯定不能生成100个ui实例, ...