小白学习go之基础篇2 -- Slice切片的原理

2023-11-13


前言

本文主要记录:
1.Slice切片的实现原理。
2.切片的指针是存储在堆中还是栈中的。
3.切片使用中的一些坑。


一、为什么要有切片?

由于go中的数组是值类型的,使用的时候是固定大小的 如arr := [3]int{1,2,3} 之后就无法再改变数组的长度了。
所以golang基于数组设计出了以动态数组为底层数据结构的切片Slice。动态数组就是一种可以动态往数组中添加、删除元素的数据结构,并且在容量不足的时候可以动态的进行扩容。

二、切片是怎么实现的呢?

1.Go的切片结构体 – SliceHeader

如下图所示,go使用SliceHeader这个struct来表示动态数组,它包含了一个Data 的uint类型的指针用于指向底层动态数组的地址、Len表示切片中保存的元素个数、Cap表示当前数组内总共有多少容量。

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

2.初始化切片的两种方式

切片初始化的方式分为 编译期初始化运行时初始化两种方式:
编译期初始化包括了: 1)从数组中生成切片 2)使用字面量的方式生成切片

// 演示go语言切片相关操作
// slice初始化的方式一: []string{}
slice1 := []string{"java", "golang", "python", "php"}
fmt.Println(slice1) // [java golang python php]

// slice初始化的方式二: 使用make() 函数 很常用!
slice2 := make([]string, 5)
fmt.Println(len(slice2)) // 5
fmt.Println(slice2) //[    ] 默认是5个空字符串

// slice初始化的方式三: 通过数组来创建
data_arr := [5]int{2,4,6,8,10}
slice3 := data_arr[1:4]
fmt.Println(slice3) // [4 6 8]

编译期初始化切片的方式可以在编译期就把元素的类型、长度等需要做的typeCheck都提前做了,所以当我们声明了一个长度只有4的数组,而要通过数组的方式来获得一个长度为5的切片是可以在build编译期就把错误提示出来的。

*编译器初始化切片的原理底层实际上是这样的:

slice := []int{1,2,3}
//实际上它会经历一下的几个步骤:
// 计算出元素的个数,然后初始化一个数组出来
var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
// 创建一个数组的指针
var vauto *[3]int = new([3]int)
// 数组指针指向上面的数组
*vauto = vstat
// 用户声明的slice变量指向该数组,也就是回到第一步从数组的方式来初始化
slice := vauto[:]

*运行期使用make函数初始化有两种方式: make([]int, 5) 和 make([]int,5,6)
第一种指定了Len长度而没有指定Cap长度的切片生成出来的就是一个带有5个int默认值的元素并且Cap也是5的切片。
第二种生成了cap为6,那么底层就会开辟6个空间,但是只有5个元素。

3.切片是在栈上分配内存的还是在堆?

这个与:

  • 1.切片的容量有关。
    当切片的容量非常小的时候,直接在栈上分配内存,如果非常大则会直接在堆上分配内存,这点与数组是类似的。

  • 2.切片指针的变量是否发生了逃逸
    我们可以使用go build --gcflags来观察变量内存的分配过程:

// go build -gcflags '-m -l' array.go
// ./array.go:5:2: moved to heap: s
// ./array.go:5:11: make([]int, 3, 4) escapes to heap

func test() *[]int {

   s := make([]int,3,4)

   return &s
}

//再进行实验发现,以下场景是不会发生s的变量逃逸的
// ./array.go:5:11: make([]int, 3, 4) does not escape

func test() int {

   s := make([]int,3,4)

   return s[0]
}

// 但是如果test方法返回的是s这个切片,仍然会发生逃逸,可能这属于编译器无法确定是否被外部引用的场景
// ./array.go:5:11: make([]int, 3, 4) escapes to heap
func test() []int {

   s := make([]int,3,4)

   return s
}

发现在内存分配的过程中,s这个变量逃逸到了堆中去初始化,也就是说这个切片的指针是存储在堆上的,而对于切片本身是存储在堆上还是栈上,如果切片指针的变量都发生了逃逸,那么切片也会在堆上初始化,尽管它的容量很小本可以在栈上就完成初始化。

总结:
经过学习,这与代码片段的上下文环境有关系:
1)如果变量明确被函数外部所引用,那么肯定会在堆上分配内存
2)如果编译期编译器不能确定是否被外部引用,也会直接分配在堆上.
3)但是如果切片在方法中初始化之后,只用于取其中一部分的值返回,仍然不会发生逃逸。

4.切片的扩容:

slice的扩容
slice的扩容要注意两个点:
1.扩容的原理 2.扩容机制
1.扩容的原理:
实际上扩容是会重新申请存储空间,空间长度为扩容后的cap,然后把原来len长度的数据拷贝到新的切片中,原来的切片依然存在。
2.扩容机制:
当cap小于1024时,基本上当slice满足了cap后再append进新的数据都会扩容原来的cap的两倍
当cap大于1024后,slice的扩容方式为原来cap的1.25倍,也就是在原来的基础上开辟多1/4的新空间
// 声明一个数组
arr := [10]int{1,2,3,4,5,6,7,8,9,10}
// 从数组中产生了第一个切片
sub_slice1 := arr[2:4]
// 发现它的len是2 cap是8
// 这说明切面会从原数组中复用它的cap作为存储空间,不会再新申请空间,节省空间
fmt.Printf("len:%d, cap:%d \n", len(sub_slice1), cap(sub_slice1)) //len:2, cap:8

// 下面演示2倍的简单扩容
// 首先申请一个空的切片
empty_slice := make([]int,0)
fmt.Printf("len:%d, cap:%d \n", len(empty_slice), cap(empty_slice)) //len:0, cap:0
new_slice := append(empty_slice, 1)
fmt.Printf("len:%d, cap:%d \n", len(new_slice), cap(new_slice)) //len:1, cap:1
new_slice = append(new_slice, 2)
fmt.Printf("len:%d, cap:%d \n", len(new_slice), cap(new_slice)) //len:2, cap:2
new_slice = append(new_slice, 3)
new_slice = append(new_slice, 4)
fmt.Printf("len:%d, cap:%d \n", len(new_slice), cap(new_slice)) //len:4, cap:4
// 下一次扩容可以清晰看到2倍
new_slice = append(new_slice, 5)
fmt.Printf("len:%d, cap:%d \n", len(new_slice), cap(new_slice)) //len:5, cap:8

三、切片的使用有什么坑需要注意呢?

实际开发中,我遇到的使用切片的坑:
1.当从一个数组生成很多切片的时候,要注意每个切片的底层实际上都是同一个数组,更新操作会影响到别的切片看到的切片内容。
2.当我们希望在别的方法中对切片进行新增append操作时,必须返回一个切片然后用变量接收,才能完成切片的增删操作,否则会无效。
3.大切片问题,大切片在扩容或者copy的时候性能会很低,要注意大切片的append操作一旦动态新增内存时超出了限制,就会导致程序崩溃;而copy一个大切片也会很慢。


总结

切片是go开发中非常常用的一种动态数组的类型,除了简单的运用外,了解它的原理将有助于我们在开发中少踩坑更有效率的完成开发。
由于go语言的原生库没有链表也没有队列、栈等更高级的数据结构,我们可以自行基于切片(也就是动态数组)实现队列和栈这样的结构,在实现数据结构的增删改查的过程中对Slice可以有更深刻的认识。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

小白学习go之基础篇2 -- Slice切片的原理 的相关文章

  • 为什么 golang 堆配置文件中的“Total MB”小于顶部的“RES”?

    我有一个用 go 编写的服务 在运行时需要 6 7G 内存 RES 在顶部 所以我使用 pprof 工具试图找出问题所在 go tool pprof pdf http
  • gcloud 部署应用程序找不到导入包 - golang

    我已经将应用程序的一个版本部署到 GAE 但现在部署新版本时遇到问题 当我尝试时gcloud app deploy version VERSION 我收到一堆错误 显示远程构建找不到我的导入包 Beginning deployment of
  • 在 Go 中获取机器 epsilon 的最简单方法

    在 Go 中获取机器 epsilon 的最简单方法是什么 浮点数的其他方面 例如精度 最小指数 最大指数 摆动等 又如何呢 我意识到有一个 math const 包 其中包含不同浮点类型的最大值和最小值 http golang org sr
  • go:找到模块但不包含包

    我正在尝试安装 go 的网络包 但收到 不包含包错误 终端截图 我咨询过 go 模块 latest 已找到但不包含包 https stackoverflow com questions 62974985 go module latest f
  • 地图中的最大元素数

    GO 中的 Map 最多可以存储多少个元素 如果我需要经常从 Map 访问数据 那么在长时间运行的程序中不断向 Map 添加项目并从中检索项目是一个好主意吗 除了map length类型的最大值之外 map中的元素数量没有理论上的限制 in
  • golang:使用 gin 路由器服务 net.Conn

    我有一个处理传入 TCP 连接的函数 func Handle conn net Conn error 另外 我有一个初始化的 gin 路由器 带有已实现的句柄 router gin New router GET router POST Th
  • 在 Go 中使用 init() 函数真的很糟糕吗?

    几天前我开始了一个新的 go 项目 我使用 golangci lint 使我的代码具有良好的风格 我发现 gochecknoinits 是 golangci lint 的 linter 之一 它告诉我不要使用 init 在我看来 为了方便起
  • 当所有通道都关闭时中断 select 语句

    我有两个独立生成数据的 goroutine 每个将其发送到一个通道 在我的主 goroutine 中 我想在每个输出进入时使用它们 但不关心它们进入的顺序 每个通道在耗尽其输出时都会自行关闭 虽然 select 语句是像这样独立使用输入的最
  • 在 Go 中修改导入的库

    我的问题 弹性节拍 https www elastic co products beats是一个用 Go 编写的日志传送程序的开源项目 它具有多种日志输出功能 包括控制台 Elasticsearch 和 Redis 我想将我自己的输出添加到
  • 如何使用json传递opentracing数据

    我的 API 网关启动一个跟踪器和一个用于验证电子邮件的范围 然后它传递给user service用于验证 我想通过这个span详情至user service作为 json 对象并启动另一个span as a tracer start sp
  • 使用 MongoDB Atlas 时 mongo-go-driver 因服务器选择超时而失败

    去版本 1 12 5 我有这个使用 node js mongo 驱动程序的代码 const MongoClient require mongodb MongoClient const uri process env MONGO HOST d
  • 如何使用golang中通过引用传递的索引访问切片中的元素

    我将切片的引用传递给函数 并且我正在函数内的切片中进行更改 我还尝试使用索引访问切片中的元素 它在 golang 中抛出异常 通过引用传递的索引访问切片中的元素的最佳方法是什么 您可以在此处找到示例代码 参考 http www reddit
  • Go中如何自定义http.Client或http.Transport超时重试?

    我想实现一个自定义http Transport对于标准http Client 如果客户端超时 它将自动重试 附 由于某种原因 习俗http Transport is a 一定有 我已经查过了hashcorp go retryablehttp
  • Go io.Pipe 的缓冲版本

    有缓冲版本吗io Pipe https golang org pkg io Pipe 在标准库或第三方库中 在我推出自己的库之前 上下文 我正在尝试使用这个解决方案 https stackoverflow com a 36229262 15
  • 是否可以获取有关 Golang 中调用者函数的信息?

    是否可以获取有关 Golang 中调用者函数的信息 例如 如果我有 func foo Do something func main foo 我怎样才能得到那个foo已被呼叫来自main 我可以用其他语言实现这一点 例如在 C 中我只需要使用
  • 有没有办法在 VSCode 中保存时运行 go 测试,并将其输出到终端?

    现在我有几个项目在VSCode中运行 运行起来相当繁琐go test每次我编写新代码时 我宁愿立即看看我是否破坏了某些东西 我知道在 Javascript 中我可以在每次保存文件时运行测试 并将输出发送到终端 现在我正在使用 保存时运行 h
  • 如何将UTC时间转换为unix时间戳

    我正在寻找将 UTC 时间字符串转换为 unix 时间戳的选项 我的字符串变量是02 28 2016 10 03 46 PM并且需要将其转换为 unix 时间戳 例如1456693426 知道该怎么做吗 首先 unix时间戳14566934
  • 解组转义 XML

    在 Go 中 我将如何解码此 XML 响应 我尝试过建立一个自定义UnMarshal方法在我的Answerstruct 但我运气不太好
  • 打印到 stdout 会导致阻塞的 goroutine 运行吗?

    作为一个愚蠢的基本线程练习 我一直在尝试实现理发师睡觉的问题 http en wikipedia org wiki Sleeping barber problem在戈兰 对于通道来说 这应该很容易 但我遇到了一个 heisenbug 也就是
  • 为什么 Go 禁止取 (&) 映射成员的地址,却允许取 (&) 切片元素?

    Go 不允许获取地图成员的地址 if I do this p mm abc Syntax Error cannot take the address of mm abc 理由是 如果 Go 允许使用此地址 那么当地图后台存储增长或缩小时 该

随机推荐

  • 如何解决主机发送地址之后 从机没有发出ACK应答信号给主机

    1 iic总线从机没有返回应答给主机 我用的一个带有iic接口电量计和我的单片机通讯 我用逻辑分析仪分析数据发现主机写一个字节数据后从机并没有返回一个ack给主机 导致我后面读取从机的数据时全部是ff 请问各位iic大神 我该怎么去查问题呢
  • 给kali的Metasploit下添加一个新的exploit

    转载 https blog csdn net SilverMagic article details 40978081 首先在 usr share metasploit framework modules exploits 目录下新建一个自
  • SpringCloud01:认识微服务

    SpringCloud01 1 认识微服务 随着互联网行业的发展 对服务的要求也越来越高 服务架构也从单体架构逐渐演变为现在流行的微服务架构 这些架构之间有怎样的差别呢 1 0 学习目标 了解微服务架构的优缺点 1 1 单体架构 单体架构
  • 从服务器拿文件,怎么从远程服务器拿文件夹

    怎么从远程服务器拿文件夹 内容精选 换一换 添加节点时提示 添加节点失败 节点已存在 待添加节点的服务器上已安装系统性能分析或者添加过节点 如果待添加节点的服务器上已安装系统性能分析 需要登录服务器卸载系统性能分析 详细步骤请参见卸载 卸载
  • [一步一步学react系列] 04—计算器Demo

    前言 之前的例子都是写的计数器 加一减一的功能 我们大致弄懂了redux分层和store数据管理 下面我们将结合现有知识写一个终极版的计算器 以此巩固所学知识 知识点 redux分层 react router 一些算法及数据结构知识 栈 中
  • 调用ChatGpt openai官方node.js包Error: connect ETIMEDOUT问题

    原因是调用的axios库不走系统代理 需要额外配置 openai在文档中有说明增加axios配置的方法 只需请求时配置下proxy就ok了
  • Understand(代码分析工具)的安装教程

    前言 最近在学习嵌入式系统时 写的代码越来越多 一个文件里面函数的数量也越来越多 为方便查看写了哪些函数 以及文件总体架构 在网上找了半天 找到了Understand这款神器 相比于vscode 该软件占内较少 查看结构更直接 文章目录 前
  • JavaScript 获取数组的最后一个元素

    index取值 args args length 1 pop方法 args pop 注意 pop方法会删除args最后一个元素 并返回
  • RS485、MODBUS通信协议浅显易懂篇

    前言 MODBUS协议是Modicon公司发表的一种串行通信协议 属于OSI模型中应用层的协议 现广泛应用于工业控制领域 它的主要特点是免费开放 支持多种电气接口 如RS 232 RS 485 传输介质可以是双绞线 光纤 无线等 RS485
  • 计算机网络基础概论

    什么是Internet 从具体构成角度看 端系统 主机节点 主机及其上运行的网络应用程序 和网络交换设备 数据交换节点 中继器 路由器 交换机 负载均衡设备等 边 通信链路 分为接入网链路和骨干链路 接入网链路是指主机连接到互联网的链路 骨
  • java开发异常类型汇总

    1 java lang nullpointerexception 这个异常大家肯定都经常遇到 异常的解释是 程序遇上了空指针 简单地说就是调用了未经初始化的对象或者是不存在的对象 这个错误经常出现在创建图片 调用数组这些操作中 比如图片未经
  • Java 5-1、用户模块-Mybatis代码生成

    5 1 用户模块 Mybatis代码生成 从这里开始 环境相关配置就告一段落了 项目就开始进入开发学习阶段 一 代码生成 实体类 Mapper接口 Mapper xml 分别生成 AppUser java SysUser java 再抽取B
  • Python题目:学生信息管理系统-高级版(图形界面+MySQL数据库)

    Python题目 学生信息管理系统 高级版 图形界面 MySQL数据库 使用图形界面显示 选用list tuple dictionary或map等数据结构 操作数据库存储X个学生的三门课的成绩 机器学习 Python程序设计 研究生英语 并
  • #BDA#笔记#阶段一:熟悉要分析的数据

    学习参考 1 小灶能力派 BDA证书班
  • java jhat_java命令--jhat命令使用

    jhat也是jdk内置的工具之一 主要是用来分析java堆的命令 可以将堆中的对象以html的形式显示出来 包括对象的数量 大小等等 并支持对象查询语言 使用jmap等方法生成java的堆文件后 使用其进行分析 第一步 导出堆 jmap d
  • 将一组很大的数据集随机分成两组数据

    最近在看机器学习的东西时发现了一些特别好玩的东西 机器学习中又分为训练集和测试集 如何把一组很大的数据分为这两个集合呢 可以使用接下来的函数完成 当然由于random这个随机数生成函数每次产生的数不一定都是刚好达到你的期望 所以总会有一点小
  • 【大数据入门核心技术-Impala】(一)Impala简介

    目录 一 Impala介绍 二 Impala优势 三 Impala主要功能 一 Impala介绍 Impala是Cloudera公司主导开发的新型查询系统 它提供SQL语义 能查询存储在Hadoop的HDFS和HBase中的PB级大数据 已
  • hibernateCRUD

    本文章的目的是实现hibernateDao层功能 但是具体的操作不在Dao层内完成 实体类 package com hibernate entity public class User private int id private Stri
  • 前端实现单元测试(代码版)

    Jest使用 下载 npm install save dev jest ts jest ts node jest globals types jest 在nodejs中支持ts ts执行报错 npx ts jest config init
  • 小白学习go之基础篇2 -- Slice切片的原理

    文章目录 前言 一 为什么要有切片 二 切片是怎么实现的呢 1 Go的切片结构体 SliceHeader 2 初始化切片的两种方式 3 切片是在栈上分配内存的还是在堆 4 切片的扩容 三 切片的使用有什么坑需要注意呢 总结 前言 本文主要记