Golang Slice 结构体

写这个的初衷是因为看到一个 b站的UP主(幼麟实验室)做 Golang 的视频有感而生,想通过视频深入剖析一下内容,顺便当作一个输出,记录自己学习的过程。

up主的连接如下:幼麟实验室

Slice 结构体

在 Golang 里面,Slice的结构体如下所示:

第一个成员为指向底层数组的指针,第二个为该切片的长度,第三个为底层数组的容量。简单的解释就是,len为这个Slice结构体可以获取数据的最大位置,而cap为指向的底层数组的实际容量。以下为slice的结构体定义:

// runtime/slice.go
type slice struct {array unsafe.Pointerlen   intcap   int
}

通过下面的方式来定义一个整数切片

var ints []int

此时,ints切片的data部分为nil,长度为0,容量为0。具体结构如[nil, 0, 0]。如果此时使用makeints开辟一个空间,如下图所示:

ints = make([]int, 2) // len = 2

make第一个参数为数组类型,第二个为切片长度。但是如果此时加入第三个参数,如下所示:

ints = make([]int, 2,5) // len = 2. cap = 5

第三个参数为切片的容量。所以,用户是可以自定义该切片的容量的。你们也可以通过如下实验来检测这个的正确性。

import "fmt"func main() {var ints []intints = make([]int, 2)fmt.Println(" len: ", len(ints), " cap: ", cap(ints))ints = make([]int, 2, 5)fmt.Println(" len: ", len(ints), " cap: ", cap(ints))
}output
>>> len: 2 cap: 2
>>> len: 2 cap: 5

刚刚已经知道,len为切片的最大可访问位置,如果我们执行以下代码,就会发生panic

func main() {var ints []intints = make([]int, 2, 5)fmt.Println("len:", len(ints), "cap:", cap(ints))fmt.Println(ints[2])
}output
>>> len: 2 cap: 2
>>> panic: runtime error: index out of range [2] with length 2

如果上述ints变量我们使用 new 来进行初始化呢,操作如下:

ints := new([]int)

此时,ints为一个指针,指向的是 slice 结构的起始地址,效果如下所示:

此时,ints指向一个datanil的空的切片。此时,如果想要为ints指向的切片开辟地址空间,可以使用append方法。如下:

ints = append(ints, 1)

此时,ints指向的切片就会变成[data, 1, 1]data为指向底层数组的指针,长度为1,容量为1。通过如下实验可以证明这个结果:

func main() {ints := new([]int)fmt.Println(&ints)fmt.Println("len:", len(*ints), "cap:", cap(*ints))*ints = append(*ints, 1)fmt.Println("len:", len(*ints), "cap:", cap(*ints))
}output
>>> 0xc0000ce018
>>> len: 0 cap: 0
>>> len: 1 cap: 1

第一个输出可以看出,ints是一个指针,此时通过指针访问切片可以知道,该切片长度为0,容量为0。通过append添加元素之后,切片长度变为1,容量也为1

Slice 结构体获取“子”切片

使用过 python 的都知道,python 数组可以直接通过索引来获得子数组,如下所示:

nums = [0,1,2,3,4]
sub = nums[1:3]
print(sub)output
>>> [1,2]

golang 中,切片也可以通过如下的方式来获取子数组。

ints := [10]int{1,2,3,4,5,6,7,8,9,10}
var s1 = ints[1:4]
var s2 = ints[7:]

通过这个方法得到的s1s2切片的底层数组指向的是ints数组。

在生成切片时,切片的长度为我们定义的长度[1:4]即为3,而切片的指针指向目标的起始位置。s1arr从索引1到索引3的数据,所以data指向arr[1]的地址,而容量则为起始位置到该底层数组的结束位置的大小,即[1-9]总共为9s2也同理。通过如下的实验可以验证这个场景。

func main() {arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}for i := 0; i < 10; i++ {println(&arr[i])}println("=====================")var s1 []int = arr[1:4]var s2 []int = arr[7:]a1 := &s1[0]println(a1)a2 := &s2[0]println(a2)
}output
>>> 0xc000077f20
>>> 0xc000077f28
>>> 0xc000077f30
>>> 0xc000077f38
>>> 0xc000077f40
>>> 0xc000077f48
>>> 0xc000077f50
>>> 0xc000077f58
>>> 0xc000077f60
>>> 0xc000077f68
>>> =====================
>>> 0xc000077f28
>>> 0xc000077f58

本次使用的电脑为64位的,所以int的大小为8个字节。可以看出,arr数组的各个元素之间的地址确实是差8个字节。分割线之后的第一个为s1底层数组指向的位置,0xc000077f28arr[1]的地址,而0xc000077f58arr[7]的位置。

重点

如果此时对s1切片进行扩容,s1的切片结构就会从[data, 3, 9]变成[data, 4, 9]。而此时,s1的指针依旧还是在arr[1]的位置上,但是此时append添加的元素就会修改在arr数组上。代码如下:

func main() {arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}fmt.Println(arr)println("=====================")var s1 []int = arr[1:4]fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))s1 = append(s1, 1)fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))println("=====================")fmt.Println(arr)
}output
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> 0xc0000b00a8  len:  3  cap:  9
>>> 0xc0000b00a8  len:  4  cap:  9
>>> =====================
>>> [0 1 2 3 1 5 6 7 8 9]

可以看出,扩容前扩容后的数组指向的都为arr[1]的地址位置,但是此时如果对s1进行扩容,因为扩容后长度仍小于容量,所以slice不会重新创建一个新的数组,而此时扩容放入的1就被赋值在了arr[4]这个位置上。此时就会对原数组数据进行修改,造成数据不安全。但是,如果扩容后的长度大于容量的话,slice就会创建一个新的数组,并将值赋值到新的底层数组上。

func main() {arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}fmt.Println(arr)println("=====================")var s1 []int = arr[7:]fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))s1 = append(s1, 1)fmt.Println(&s1[0], " len: ", len(s1), " cap: ", cap(s1))println("=====================")fmt.Println(arr)println("=====================")fmt.Println(s1)
}output
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> 0xc0000b00d8  len:  3  cap:  9
>>> 0xc0000c0090  len:  4  cap:  6
>>> =====================
>>> [0 1 2 3 4 5 6 7 8 9]
>>> =====================
>>> [7 8 9 1]

可以看出,此时扩容前后数组的底层数组位置发生了变化,且增加不再影响原来的数组了。

Slice 结构体扩容机制

从上文最后一个实验可以看出,s2在扩容之后,容量不是从3 -> 4,而是变成了6。那么,slice是如何预估扩容之后的容量呢。
slice结构体的扩容代码如下:

// runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {//...newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.cap < 1024 {newcap = doublecap} else {//...for 0 < newcap && newcap < cap {newcap += newcap / 4}}}//...
}

简而言之,如果newcap > 2 * oldcap(注意,此时这个newcap和上文代码中的newcap表达不一致,本文的newcap就是新容量的意思),那么直接将容量设置为新容量。举下面这个例子:

ints := make([]int, 2)
ints = append(ints, 2, 3, 4)

原来的容量为2,新容量为5,因为2 * 2 < 5,所以此时新的容量为5
否则,如果oldcap < 1024,则直接将容量翻倍,如果大于1024则放大1.25倍。

short_ints := make([]int, 2)
short_ints = append(ints, 2) // 这个为翻倍long_ints := make([]int, 2048)
long_ints = append(ints, 2) // 这个为 1.25 倍

但是可能会问了,上文的实验中,最后的实验产生的s2容量为6,并不是5。这个是因为,golang 在分配内存空间的时候,不会按照需要的大小去给对应的大小,而是会从内存管理模块中获取一个足够大且大小最相近的内存。因为,golang 在运行时,会预先向系统申请一部分内存,然后按照不同大小,分类缓存起来,等到需要的时候申请即可。此时不对此内容进行深究,需要的话可以看这个博主的博文。博文地址

在扩容时,申请的内存大小为新容量 * 类型大小,在如下这个实验中:

ints := make([]int, 2)
ints = append(ints, 2, 3, 4)

新的容量为5,类型大小为8个字节,所以所需的内存大小为40个字节。而实际申请时,会匹配到48字节的大小,48个字节正好是容量为6int数组的大小,所以显示出来容量为6。通过如下实验获取结果:

func main() {ints := make([]int, 2)fmt.Println(&ints[0], " len: ", len(ints), " cap: ", cap(ints))ints = append(ints, 2, 3, 4)fmt.Println(&ints[0], " len: ", len(ints), " cap: ", cap(ints))println("=====================")ints2 := make([]int, 2)fmt.Println(&ints2[0], " len: ", len(ints2), " cap: ", cap(ints2))ints2 = append(ints2, 2)fmt.Println(&ints2[0], " len: ", len(ints2), " cap: ", cap(ints2))println("=====================")ints3 := make([]int, 1024)fmt.Println(&ints3[0], " len: ", len(ints3), " cap: ", cap(ints3))ints3 = append(ints3, 2)fmt.Println(&ints3[0], " len: ", len(ints3), " cap: ", cap(ints3))
}output
>>> =====================
>>> 0xc0000180a0  len:  2  cap:  2
>>> 0xc00000a360  len:  5  cap:  6
>>> =====================
>>> 0xc0000180d0  len:  2  cap:  2
>>> 0xc00000e200  len:  3  cap:  4
>>> =====================
>>> 0xc00007c000  len:  1024  cap: 1024
>>> 0xc000100000  len:  1025  cap: 1280

从上面的第二个输出可以看出,容量满足翻倍的条件,进行了翻倍,同时匹配到的内存规格为32字节的,所以此时容量为4。而容量大于等于1024,则增长了1.25倍,且内存规格为12801280也正好是10241.25倍)。

但是可以发现ints2ints的内存地址是相同的,不知道是不是 golang 对这个做了些优化,目前我还不清楚,如果有知道的朋友也请告诉我。

最后

不得不说,前面提到的 UP 主做的视频真的很适合 golang 开发者,但是可能需要有一定理解再去看会比较有收获。还有就是,打博文太难了…

【Golang】Slice数组组成和扩容机制相关推荐

  1. hashmap 扩容是元素还是数组_HashMap的扩容机制---resize()

    面试的时候闻到了Hashmap的扩容机制,之前只看到了Hasmap的实现机制,补一下基础知识,讲的非常好 原文链接: Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复 ...

  2. arraylist扩容是创建新数组吗 java_arraylist扩容机制要怎么实现?arraylist怎么扩容...

    ArrayList大家都知道了吧,这是一个动态数组.以java语言来说,数组是定长的,在被创建之后就不能被加长或缩短了,因此,了解它的扩容机制对使用它尤为重要.下面,我们就一起来看看它的扩容机制是怎么 ...

  3. golang slice扩容机制

    Slice expanse capacity slice这种数据结构便于使用和管理数据集合,可以理解为是一种动态数组,slice也是围绕动态数组的概念来构建的.既然是动态数组,那么slice是如何扩容 ...

  4. golang slice map扩容

    golang slice 扩容 操作系统预分配的内存规格 byte 8 16 32 64 80 96 112- 先求出当前切片容量x,求出append追加后的容量 y 判断 x*2 和y 的关系 1 ...

  5. Golang Slice切片如何扩容

    Golang Slice切片如何扩容 Golang轻松学习 文章目录 Golang Slice切片如何扩容 一.Slice数据结构是什么? 二.详细代码 1.数据结构 2.扩容原则 2.如何理解扩容规 ...

  6. slice扩容机制分析

    Demo go版本:go1.18.3 在网上查阅的资料;扩容机制:当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新sli ...

  7. golang中数组和slice作为参数的区别

    最近项目中有遇到类似问题,做个记录. 举个例子,[5]int为数组,[]int为slice(数组切片),数组是值类型,而slice为引用类型,值类型作为参数传入函数,只是拷贝了个副本,修改并不会作用到 ...

  8. Golang的数组与切片——详解

    为什么需要数组 看一个问题 一个养鸡场有 6 只鸡,它们的体重分别是 3kg,5kg,1kg,3.4kg,2kg,50kg .请问这六只鸡的总体重是 多少?平均体重是多少? 请你编一个程序.=> ...

  9. 聊一聊不同技术栈中hashmap扩容机制

    前言 hash简介 作为后端开发,说HashMap是我们最经常接触到的数据结构都不为过,而HashMap如其名最主要依赖的算法就是hash散列算法来存储和读取数据.         以关键码值K为自变 ...

最新文章

  1. 博士补贴125万,硕士70万本科21万,浙江某地人才(简直是抢人)新政!
  2. R语言入门第三集 实验二:基本数据处理
  3. order by 空值排在最后_当梅根·马克尔最后一次皇室活动选择选择翡翠绿时证明她非常时髦...
  4. php 表单录入,PHP 表单和用户输入
  5. Spring+SpringMVC +MyBatis整合配置文件案例66666
  6. 0428专题:行内元素与块状元素
  7. GIS 地图制作 学习总结
  8. Android模仿超级课表,展示多门课程重叠,页面有折角背景
  9. Ubuntu16.04安装Hadoop2.7.3教程
  10. 有了这些组件和模板,天下没有难做的移动端驾驶舱
  11. matplotlib画箱线图,添加非参数检验-秩和检验的结果
  12. 更有效的编写QQ空间、CSDN、博客园图文并茂的文章
  13. 图灵奖得主、《龙书》作者最新力作:抽象、算法与编译器
  14. 树枝学术 | 图书查找、论文查找全攻略
  15. 基于python的在线音乐系统设计与实现
  16. 5.3 matlab数据插值(线性插值、最近点插值、埃尔米特插值、三次样条插值)
  17. malloc()函数与free()函数的使用
  18. MySQL中trim()函数的用法
  19. apache的url重写
  20. linux gcc忽略警告,gcc 禁止warning

热门文章

  1. 南京ibm戴尔笔记本维修
  2. Hyperledger Fabric系统架构
  3. ssdt函数索引号_【转】SSDT索引号的获取
  4. Android Kiosk 模式
  5. 一个P9告诉你为什么某电商怕了拼多多
  6. 基于asp.net的排球赛事网站设计与实现
  7. 2021蓝桥杯预选赛题解
  8. 无线网络性能测试 软件,WiFi性能测试
  9. QT项目负责人必须掌握的Ui设计师功能——Promote to !
  10. 求义隆单片机c语言红外解码程序,吐槽义隆单片机,顺便送上超轻红外解码程序....