golang切片的一些自问自答

本文探讨了Go语言中切片的数据结构,解释了为何初始化切片时应尽量指定容量,如果不设置cap,编译器如何确定默认容量,并分析了何时发生切片扩张。通过实例展示了设置cap对性能的影响,并建议在能预估容量时采用预估容量的初始化方式以优化程序性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于go切片的四个问题和回答,想哪写哪的一篇。

话说这也是2021年最后一篇了,下一篇该改签名了。


7b521abea5bbd97ae0e7c72a69e059c6.png

image-20211229094736903

你好,我是轩脉刃。这篇是关于go切片的一些问题和回答。

go的切片基本上是代码中使用最多的一种数据结构了,使用这种数据结构有哪些要注意的点,这个是非常必要了解的东西。基本上,以前写的一篇博客 https://www.cnblogs.com/yjf512/p/9531282.html  就说的很清楚了。这里再深挖一些。

问题:go的切片数据结构是什么样子的?

切片是有可能在编译器就被内联的,而如果在编译器没有被内联,进入运行期,就是直接使用SliceHeader数据结构。

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

这三个字段分别表示指针,长度,容量。

问题:为什么在初始化slice的时候尽量补全cap

当我们要创建一个slice结构,并且往slice中append元素的时候,我们可能有两种写法来初始化这个slice。

方法1:

package main

import "fmt"

func main() {
 arr := []int{}
 arr = append(arr, 1,2,3,4, 5)
 fmt.Println(arr)
}

方法2:

package main

import "fmt"

func main() {
   arr := make([]int, 0, 5)
   arr = append(arr, 1,2,3,4, 5)
   fmt.Println(arr)
}

方法2相较于方法1,就只有一个区别:在初始化[]int slice的时候在make中设置了cap的长度,就是slice的大小。

这两种方法对应的功能和输出结果是没有任何差别的,但是实际运行的时候,方法2会比少运行了一个growslice的命令。

这个我们可以通过打印汇编码进行查看:

方法1:

4b7e6414755bc8c644f60c081345c8de.png
image-20211219173237557

方法2:

eec07a2f03040ed92c9e38dc8ad82ecd.png
image-20211219174112164

我们看到方法1中使用了growsslice方法,而方法2中是没有调用这个方法的。

这个growslice的作用就是扩充slice的容量大小。就好比是原先我们没有定制容量,系统给了我们一个能装两个鞋子的盒子,但是当我们装到第三个鞋子的时候,这个盒子就不够了,我们就要换一个盒子,而换这个盒子,我们势必还需要将原先的盒子里面的鞋子也拿出来放到新的盒子里面。所以这个growsslice的操作是一个比较复杂的操作,它的表现和复杂度会高于最基本的初始化make方法。对追求性能的程序来说,应该能避免尽量避免。

具体对growsslice函数具体实现同学有兴趣的可以参考源码src的 runtime/slice.go 。

当然,我们并不是每次都能在slice初始化的时候就能准确预估到最终的使用容量的。所以这里使用了一个“尽量”。明白是否设置slice容量的区别,我们在能预估容量的时候,请尽量使用方法2那种预估容量后的slice初始化方式。

问题:如果不设置cap,make slice的时候,创建的cap为多大?

如果不设置cap,不管是使用make,还是直接使用[]slice 进行初始化,编译器都会计算初始化所需的空间,使用最小化的cap进行初始化。

a := make([]int, 0)  // cap 为0
a := []int{1,2,3} // cap 为3

可以从ssa看出

140bfd37d86e8d1074da1925fb4dbd9a.png
image-20211221095655104

问题:slice什么时候决定扩张?

之前写过一篇文章 https://www.cnblogs.com/yjf512/p/10714792.html 里面得出的结论就是slice在编译期就决定是否要调用growslice。

这个逻辑是正确的。

编译器在ssa的时候 对于append是会转换为 OAPPEND(cmd/compile/internal/typecheck/universe.go) 。而在 cmd/compile/internal/ssagen/ssa.go 中,对其进行判断。

c4eedadbd813157edfe0fc5722d6c5c0.png
image-20211221101614497

目前还看不懂下面append下面的逻辑,不过基于这个注释,能了解到这里growslice的逻辑。比较扩容前后大小,如果原先cap小于扩容后需要cap,就growslice。

总结

琢磨了四个关于切片的问题:

问题:go的切片数据结构是什么样子的?

问题:为什么在初始化slice的时候尽量补全cap?

问题:如果不设置cap,make slice的时候,创建的cap为多大?

问题:slice什么时候决定扩张?

感觉第四个问题还没想透。

697de528c6e35b552d065d05ebac33d2.png

Hi,我是轩脉刃,一个名不见经传码农,体制内的小愤青,躁动的骚年,2021年想坚持写一些学习/工作/思考笔记,谓之倒逼学习。欢迎关注个人公众号:轩脉刃的刀光剑影。

d5c0ba65d65f86cf9f3df4b6fde4ddde.png

MORE | 更多原创文章

### Golang切片的底层实现原理 在 Golang 中,切片是一种动态数组结构,它提供了灵活的方式来操作连续内存区域的数据。尽管切片看起来像数组的一部分,但它实际上是一个指向底层数组的描述符[^2]。 #### 切片的内部结构 切片由三个部分组成:`指针`、`长度` 和 `容量`。具体来说: - **指针** (`Pointer`):指向底层数阵列的第一个元素。 - **长度** (`Length`):表示当前切片中有效元素的数量。 - **容量** (`Capacity`):表示从第一个元素到底层数组最后一个可用位置之间的总元素数量。 这种设计使得切片可以轻松扩展其大小而必重新分配整个数据集。当需要更多空间时,可以通过内置函数 `append` 动态调整切片的容量并复制原有数据至新的更大的底层数组中。 #### 底层存储机制 切片直接拥有自己的数据,而是共享同一个底层数组的内容。这意味着多个同的切片可能引用同一块内存的同片段。因此,在修改某个切片中的元素时,如果这些元素被其他切片所共用,则那些切片也会受到影响[^3]。 以下是创建和初始化一个简单切片的例子以及它的底层表现形式: ```go package main import "fmt" func main() { var s []int = make([]int, 5, 10) // 创建了一个初始长度为5,容量为10的整数切片 fmt.Printf("len=%d cap=%d slice=%v\n", len(s), cap(s), s) for i := range s { s[i] = i * 2 } fmt.Println(s) } ``` 在这个例子中,我们通过调用 `make()` 函数来显式地定义一个新的切片及其属性(即长度与容量)。随后填充了一些数值进去,并打印出来验证结果是否符合预期。 #### 扩展与重分配过程 一旦尝试向已满的切片追加新项超出其现有能力范围之外时,程序会自动执行以下动作之一以适应新增需求: 1. 如果原容量足够大则仅更新相应字段即可; 2. 否则将构建一块全新的更大尺寸的新区划给定对象关联起来同时保留旧有资料副本过去继续沿用直至完全废弃为止; 此过程中涉及到了复杂的逻辑判断及资源管理策略确保效率最大化的同时兼顾安全性考虑防止越界访问等问题发生[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值