首页 > 系统相关 >Go进阶概览 -【2.4 切片的结构与内存管理】

Go进阶概览 -【2.4 切片的结构与内存管理】

时间:2024-09-08 21:52:55浏览次数:14  
标签:扩容 进阶 容量 切片 数组 Go 长度 底层 2.4

2.4 切片的结构与内存管理

切片是我们日常使用比较多的一个结构,深入的了解它的结构对于我们提高程序性能也有比较大的帮助。

本节我们将针对切片底层结构、扩容机制、底层数组进行讲解。

本节代码存放目录为 lesson4

切片底层结构

我们在使用的时候发现切片与数组很相似,这是由于本身切片的底层其实就是由数组构成的。主要结构如下:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  • array:指向底层数组的指针。

  • len:长度,切片实际存储元素的数量。

  • cap:容量,切片最大可以存储元素的数量。

从上面的结构可以看出,切片的底层其实也是数组。

当新创建一个切片时,会同步创建一个底层数组,之后在array中存储底层数组的指针,那么在访问的时候,其实也是访问的最底层的这个数组。

那么切片结构本身在内存底层是怎么表示的呢?由于切片结构本身在底层是一个结构体,所以其实它的内存表示就是按照结构体的方式来进行的。

扩容机制

上文中我们提到了len长度与cap容量,长度是比较好理解的,这也是数组中的概念。

那么容量又是什么呢?首先我们回顾一个概念:数组长度是固定的,不可改变;切片的长度是可变的。

那么既然数组不可变化,切片的底层也是数组,又是怎么实现长度可变的呢?这就涉及到了容量的概念。

Go语言中,其实是 通过不断的创建底层新数组实现长度可变的。


我们举个例子,比如目前切片的长度是5,底层数组的长度也是5,接下来我需要通过append新添加一个元素,由于底层数组长度不可变,那么要怎么办呢?

Go语言中,这时候就会新创建一个数组,将之前的5个元素拿到新数组,同时将新添加的这个元素也放到新数组,之后更新切片结构的array指针指向新的数组。

这样操作是很便捷的,但是每次都创建新的数组,如果数据比较多的时候,这个开销也是不小的。

所以Go语言提出了容量的概念,也就是说:创建新数组的时候,多创建几个空位,那么之后再添加就不用重复的创建新数组了

我们可以通过下面的代码查看:

a := []int{1, 2, 3, 4, 5}
fmt.Printf("切片a长度: %d, 容量: %d\n", len(a), cap(a))
a = append(a, 6)
fmt.Printf("切片a长度: %d, 容量: %d\n", len(a), cap(a))

结果输出如下所示:

切片a长度: 5, 容量: 5
切片a长度: 6, 容量: 10

从上面的示例我们可以看到,当我们创建切片时,由于是知道元素数量的,所以第一次创建的时候容量就是5

新添加元素的时候,由于底层数组的长度只有5,是不够存储的,所以可以看到容量变成了10

我们可以验证一下,底层数组长度是否是10,代码如下:

hdr := (*reflect.SliceHeader)(unsafe.Pointer(&a))
arrayPtr := hdr.Data
array := (*[10]int)(unsafe.Pointer(arrayPtr))
fmt.Println("底层数组: ", array)

结果输出如下所示:

底层数组:  &[1 2 3 4 5 6 0 0 0 0]

从上面的输出我们可以看出,扩容后底层数组的长度就是10,后面的位置则是默认值,也就是还没有使用的。


除了上面说的,还有另一种特殊的情况。如下代码:

b := [5]int{1, 2, 3, 4, 5}
bs := b[1:4]
fmt.Printf("切片bs index 0: %d, 切片bs长度: %d, 容量: %d\n", bs[0], len(bs), cap(bs))

结果输出如下所示:

切片bs index 0: 2, 切片bs长度: 3, 容量: 4

在上面的代码中,我们从数组b中截取了部分形成了切片bs,最终输出的长度是3,容量是4

执行上面的代码,最终bs的元素是:2,3,4。那么如果根据上面说的,这时候创建了新切片,底层也会创建新的数组,那容量就应该是3,为什么会是4呢?

这是因为在这种情况的时候,切片其实并没有创建新的数组,而是指向数组b的索引1内存地址的,这时候从索引1以后一共有1,2,3,4四个位置,所以容量是4而不是3

这也就是容量的核心概念:切片起始位置到底层数组结束位置的长度。

扩容规则

上面我们讲到了扩容的机制,那么扩容的规则又是怎么样的呢?我们通过下面的代码看一下:

c := []int{1, 2, 3, 4, 5}
fmt.Printf("切片c长度: %d, 容量: %d\n", len(c), cap(c))
c = append(c, 6)
fmt.Printf("切片c长度: %d, 容量: %d\n", len(c), cap(c))
c = append(c, 7)
fmt.Printf("切片c长度: %d, 容量: %d\n", len(c), cap(c))
c = append(c, 8, 9, 10, 11)
fmt.Printf("切片c长度: %d, 容量: %d\n", len(c), cap(c))

结果输出如下所示:

切片c长度: 5, 容量: 5
切片c长度: 6, 容量: 10
切片c长度: 7, 容量: 10
切片c长度: 11, 容量: 20

从上面的结果我们可以看出,在容量不够的时候,都是将容量扩大到两倍

那么是否一直都是这样呢?如果一直这样的话容量就会变得特别大。我们接着看下面的代码:

for i := 0; i < 2000; i++ {
	s = append(s, i)
	capNew := cap(s)
	if capNew != capOld {
		fmt.Printf("扩容: 旧容量=%d, 新容量=%d\n", capOld, capNew)
		capOld = capNew
	}
}

结果输出如下所示:

go1.19
扩容: 旧容量=5, 新容量=10
扩容: 旧容量=10, 新容量=20
扩容: 旧容量=20, 新容量=40
扩容: 旧容量=40, 新容量=80
扩容: 旧容量=80, 新容量=160
扩容: 旧容量=160, 新容量=336
扩容: 旧容量=336, 新容量=672
扩容: 旧容量=672, 新容量=1184
扩容: 旧容量=1184, 新容量=1696
扩容: 旧容量=1696, 新容量=2384

go1.15
扩容: 旧容量=5, 新容量=10
扩容: 旧容量=10, 新容量=20
扩容: 旧容量=20, 新容量=40
扩容: 旧容量=40, 新容量=80
扩容: 旧容量=80, 新容量=160
扩容: 旧容量=160, 新容量=336
扩容: 旧容量=336, 新容量=672
扩容: 旧容量=672, 新容量=1360
扩容: 旧容量=1360, 新容量=1792
扩容: 旧容量=1792, 新容量=2304

从上面的输出我们可以发现,当容量增长到672以后,就没有按照2倍的规则进行了,同时使用不同的Go版本扩容规则也是不一样的。

如果再换不同的Go语言版本可能输出还会不一样,但是他们是有相同点的:在容量小于1024时,会按照2倍扩展;大于1024时,会根据内存占用、内存对齐等方式平滑扩容

也就是说,当前容量大于1024后,就不会按照2倍那样的去扩容了,而是会平滑的扩容,避免扩容过多。

基于扩容机制及规则,那么我们在使用切片的时候,就应该考虑到长度的计算,避免扩容太频繁造成内存的浪费。

小结

本节我们讲解了切片的底层结构、扩容机制及扩容规则,如果感兴趣的话,我们还可以再去看一下它的底层内存表示。

关于本节总结如下:

  • 切片的底层结构是数组

  • 切片通过创建新数组的方式实现长度可变

  • 切片扩容就是创建新数组

我的GitHub:https://github.com/swxctx

书籍地址:https://d.golang.website/

书籍代码:https://github.com/YouCanGolang/GoDeeperCode

标签:扩容,进阶,容量,切片,数组,Go,长度,底层,2.4
From: https://blog.csdn.net/qq_28796345/article/details/142033951

相关文章

  • Linux目录结构进阶和过滤命令(三)
    1.日志查询四剑客注意:查看日志的时候不要用cat或者vim命令,在工作中日志的内容很多,用cat会刷屏,用vim又特别的占用内存,所以我们引出了四条有关查看日志的相关命令1.1四剑客之headhead#显示文件的头几行,默认显示十行head-nnum#显示头num行实例一:显示/etc/passwd的......
  • 非官方python二进制包 https://www.lfd.uci.edu/~gohlke/pythonlibs/ 替代
    前两年的时候,由于偶尔会使用LFD中的二进制python包,但是下载地址都是加密的,不能直接给pip使用,因此为了方便自己把地址解密后做了一个目录页,并自动更新。今天看了一下页面发现包的更新时间都是前两年的,以为是自动更新程序出问题了,一番求证后发现原来是LFD的服务关闭了,幸好只关闭了......
  • 【Python使用】嘿马python高级进阶全体系教程第9篇:HTTP 协议,1. HTTP 协议的介绍【附
    本教程的知识点为:操作系统1.常见的操作系统4.小结ls命令选项2.小结mkdir和rm命令选项1.mkdir命令选项压缩和解压缩命令1.压缩格式的介绍2.tar命令及选项的使用3.zip和unzip命令及选项的使用4.小结编辑器vim1.vim的介绍2.vim的工作模式3.vim的末行模......
  • Python毕业设计基于Django的川剧戏剧京剧戏曲科普平台 含选座功能
    文末获取资源,收藏关注不迷路文章目录一、项目介绍1管理员功能模块前台系统功能模块二、主要使用技术三、研究内容四、核心代码五、文章目录一、项目介绍随着我国经济的高速发展与人们生活水平的日益提高,人们对生活质量的追求也多种多样。尤其在人们生活节奏不断加......
  • 【Django开发】django美多商城项目完整开发4.0第10篇:收货地址,数据库建表【附代码文档
    本教程的知识点为:项目准备项目准备配置1.修改settings/dev.py文件中的路径信息2.INSTALLED_APPS3.数据库用户部分图片1.后端接口设计:视图原型2.具体视图实现用户部分使用Celery完成发送判断帐号是否存在1.判断用户名是否存在后端接口设计:用户部分JWT什......
  • Go语言中的RPC协议原理解析
    Go语言中的RPC协议原理解析在分布式系统中,不同的服务或组件通常运行在不同的计算机或进程上。为了实现这些服务之间的通信,我们可以使用RPC(RemoteProcedureCall,远程过程调用)协议。RPC允许我们像调用本地函数一样调用远程服务,从而简化了分布式系统中的通信复杂性。本文将详......
  • 一个小例子,给你讲透典型的 Go 并发操作
    一个小例子,给你讲透典型的Go并发操作原创 訢亮 程序员新亮  2024年09月08日16:57 天津 听全文程序员新亮GitHub9K+Star|技术交流分享206篇原创内容公众号如果你有一个任务可以分解成多个子任务进行处理,同时每个子任务没有先后执行顺序的限制......
  • 利用Django框架快速构建Web应用:从零到上线
    随着互联网的发展,Web应用的需求日益增长,而Django作为一个高级的PythonWeb框架,以其强大的功能和灵活的架构,成为了众多开发者的选择。本文将指导你如何从零开始使用Django框架构建一个简单的Web应用,并将其部署到线上,让世界看到你的作品。Django简介Django是由AdrianHolov......
  • 网络属性及相关配置工具\shel脚本编程-进阶 \进程-系统性能和计划任务
    一、通过网络配置命令让主机上网1.查看网络接口信息:  -`ipa`或者`ifconfig`显示系统中所有网络接口的详细信息,包括IP地址、子网掩码、MAC地址等。2.配置IP地址、子网掩码、网关和DNS:  -IP地址:使用`ifconfig`或`ipaa`命令来设置IP地址。例如,`ifconfig......
  • ginkgo编写测试用例
    gogetgithub.com/onsi/ginkgo/v2/ginkgogoinstallgithub.com/onsi/ginkgo/v2/ginkgogogetgithub.com/onsi/gomegamkdirtestcdtestginkgobootstrapginkgo常用的模块是It、Describe、BeforeEach、AfterEach、BeforeSuite、AfterSuite。It指定单个测试用例。Describe......