Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Golang切片与实现原理

Golang切片与实现原理

作者头像
Orlion
发布于 2024-09-02 08:33:45
发布于 2024-09-02 08:33:45
10100
代码可运行
举报
文章被收录于专栏:我的独立博客我的独立博客
运行总次数:0
代码可运行

本文Golang版本为1.13.4

Slice底层结构

go中切片实际是一个结构体,它位于runtime包的slice.go文件中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

array是切片用来存储数据的底层数组的指针,len为切片中元素的数量,cap为切片的容量即数组的长度

切片的初始化

创建一个切片有以下几种方式

1. 通过字面量创建

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
arr1 := [3]int{1,2,3} // 创建一个数组
s1 := []int{1,2,3} // 创建一个len为3,cap为3的切片

上面的创建方式非常容易与数组的另一个创建方式弄混

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
arr2 := [...]int{1,2,3} // 创建一个数组,数组长度由编译器推断

s1在内存上的结构如下图:

2. 通过make()函数创建

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s1 := make([]int, 10) // 创建一个长度为10,容量为10的切片
s2 := make([]int, 5, 10) // 创建一个长度为5,容量为5的切片

s2的内存结构如图:

3. 通过数组/切片创建另一个切片

通过数组/切片创建另一个切片语法为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
slice[i:j:k]

其中i表示开始切的位置,包括该位置,如果没有则表示从0开始切;j表示切到的位置,不包括该位置,如果没有j则切到最后;k控制切片的容量切到的位置,不包括该位置,如果没有则切到尾部。下面举几个例子说明:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a := [10]int{0,1,2,3,4,5,6,7,8,9}
s1 := a[2:5:9] // s1结果为[2,3,4], len:3, cap:7
s2 := a[2:5:10] // s2结果为[2,3,4] len:3, cap:8
s3 := a[2:7:10] // s3结果为[2,3,4,5,6] len:5, cap:8
s4 := a[2:] // s4结果为[2,3,4,5,6,7,8,9] len:8, cap:8
s5 := a[:3] // s5结果为[0,1,2] len:3, cap:10
s6 := a[::3] // 编译报错: middle index required in 3-index slice
s7 := a[:] s7结果为[0,1,2,3,4,5,6,7,8,9], len:10, cap:10
s10 := s1[1:3] s10结果为[3,4], len:2, cap: 6。注意s10的cap是6,而不是7

s1与s10在内存上的结构如图:

由于as1s2s3s4s5s7共享同一个数组,所以其中任意一个变量通过索引修改了底层数组元素的值,相当于修改了以上所有变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s2[3] = 30

执行上面的代码后:变量a变成了[0,1,2,30,4,5,6,7,8,9]、s1变成了[2,30,4]、…… s7变成了[0,1,2,30,4,5,6,7,8,9]

nil切片与空切片

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var s11 []int
var s12 = make([]int, 0)

上面的s11为nil,s12是空切片,他们在内存上的结构如图:

我写了段代码验证了下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var s10 = make([]int, 0)
sh10 := (*reflect.SliceHeader)(unsafe.Pointer(&s10))
println(unsafe.Pointer(sh10.Data))
var s11 []int
sh11 := (*reflect.SliceHeader)(unsafe.Pointer(&s11))
println(unsafe.Pointer(sh11.Data))
var s12 = make([]int, 0)
sh12 := (*reflect.SliceHeader)(unsafe.Pointer(&s12))
println(unsafe.Pointer(sh12.Data))
var s13 = make([]int, 0)
sh13 := (*reflect.SliceHeader)(unsafe.Pointer(&s13))
println(unsafe.Pointer(sh13.Data))

打印结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0xc00006af08
0x0
0xc00006af08
0xc00006af08

根据打印结果可以看到是上面的结构无误

切片创建源码

我们打印下下面代码对应的汇编,看下golang是如何为我们创建出来一个切片的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	tttttt := make([]int, 999)
	fmt.Println(tttttt)
}

通过go tool compile -S -l slice.go打印对应汇编(-l是禁止内联),下面只摘取关键部分

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"".main STEXT size=181 args=0x0 locals=0x48
	...
	// 栈增加72个字节
        0x0013 00019 (slice.go:5)       SUBQ    $72, SP
	// 将当前栈底地址加载到到当前栈顶地址+64处
        0x0017 00023 (slice.go:5)       MOVQ    BP, 64(SP)
	// 栈底修改为栈顶地址+64
        0x001c 00028 (slice.go:5)       LEAQ    64(SP), BP
        ...
        0x0021 00033 (slice.go:6)       LEAQ    type.int(SB), AX
        ...
	// 下面三行实际是把tuntime.makeslice放到栈上的指定位置
        0x0028 00040 (slice.go:6)       MOVQ    AX, (SP)
        0x002c 00044 (slice.go:6)       MOVQ    $999, 8(SP)
        0x0035 00053 (slice.go:6)       MOVQ    $999, 16(SP)

上面的部分画个图可能更清晰些:

继续看汇编:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	// 调用runtime.makeslice函数
        0x003e 00062 (slice.go:6)       CALL    runtime.makeslice(SB)
        ...
	// 将返回值加载到AX寄存器
        0x0043 00067 (slice.go:6)       MOVQ    24(SP), AX
        ...
	// 下面就是调用fmt.Println函数的代码了
        0x0048 00072 (slice.go:7)       MOVQ    AX, (SP)
        0x004c 00076 (slice.go:7)       MOVQ    $999, 8(SP)
        0x0055 00085 (slice.go:7)       MOVQ    $999, 16(SP)
        0x005e 00094 (slice.go:7)       CALL    runtime.convTslice(SB)
        ...
        0x0063 00099 (slice.go:7)       MOVQ    24(SP), AX
        ...
        0x0068 00104 (slice.go:7)       XORPS   X0, X0
        0x006b 00107 (slice.go:7)       MOVUPS  X0, ""..autotmp_1+48(SP)
        ...
        0x0070 00112 (slice.go:7)       LEAQ    type.[]int(SB), CX
        ...
        0x0077 00119 (slice.go:7)       MOVQ    CX, ""..autotmp_1+48(SP)
        ...
        0x007c 00124 (slice.go:7)       MOVQ    AX, ""..autotmp_1+56(SP)
        ...
        0x0081 00129 (slice.go:7)       LEAQ    ""..autotmp_1+48(SP), AX
        ...
        0x0086 00134 (slice.go:7)       MOVQ    AX, (SP)
        0x008a 00138 (slice.go:7)       MOVQ    $1, 8(SP)
        0x0093 00147 (slice.go:7)       MOVQ    $1, 16(SP)
        0x009c 00156 (slice.go:7)       CALL    fmt.Println(SB)
        0x00a1 00161 (slice.go:8)       MOVQ    64(SP), BP
        0x00a6 00166 (slice.go:8)       ADDQ    $72, SP
        0x00aa 00170 (slice.go:8)       RET
        0x00ab 00171 (slice.go:8)       NOP
        ...
        0x00ab 00171 (slice.go:5)       CALL    runtime.morestack_noctxt(SB)
        ...
        0x00b0 00176 (slice.go:5)       JMP     0

上面出现了一个关键函数,即runtime.makeslice,(在堆上分配时才会调用这个函数)我们看下它的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func makeslice(et *_type, len, cap int) unsafe.Pointer {
	// 这里实际是计算切片所占的内存大小,即元素的大小乘容量
	// mem为所需内存大小,overflow标识是否溢出
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		// 如果溢出或者所需内存大于最大可分配内存或者len、cap不合法则报错
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}
	// 调用mallocgc从go内存管理器获取一块内存
	return mallocgc(mem, et, true)
}

函数传参

切片作为函数参数传参时实际上是复制了一个runtime.slice结构体,而非是传递的runtime.slice结构体指针,举个栗子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	slice := []int{0,1,2}
	foo(slice)
}
func foo(slice []int) {
	...
}

其实就等价于

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Slice struct {
	ptr *[3]int
        len int
	cap int
}

func main() {
	slice := Slice{&[3]int{1,2,3}, 0, 0}
	foo(slice)
}
func foo(slice Slice) {
	...
}

因为函数的形参与实参共享同一个数组,这就导致当把一个切片作为参数传递到另一个函数时,在函数内修改形参的某个下标的值时也会修改了实参。描述的比较绕,下面看一个实例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	param := []int{0, 1, 2}
	foo(param)
	fmt.Println(param)
}

func foo(arg []int) {
	arg[1] = 10
}

打印结果为[0,10,2],原因是param与arg共享同一个底层数组,函数foo内修改了arg[1]实际是将两者的底层数组下标为1的元素修改为了10,所以main函数中的param[1]也就变成了10。 在foo函数内修改arg的len字段,是不会影响到param的len的,下面我们验证下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	param := []int{0, 1, 2}
	foo(param)
	fmt.Println(param)
	fmt.Println(len(param))
}

func foo(arg []int) {
	arg[1] = 10
	argSlice := (*reflect.SliceHeader)(unsafe.Pointer(&arg))
	argSlice.Len = 10
	fmt.Println(len(arg))
}

打印结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
10
[0 10 2]
3

验证成功。

切片扩容

当通过append函数向切片中添加数据时,如果切片的容量不足,需要进行扩容,实际调用的是runtime包中的growslice()函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
	...

	// 下面就是计算新容量的部分了
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		// 如果所需容量大于当前容量的两倍,则新容量为所需容量
		newcap = cap
	} else {
		// 下面是所需容量<=当前容量两倍的逻辑
		if old.len < 1024 {
			// 如果当前长度<1024则新容量为当前容量x2
			newcap = doublecap
		} else {
			// 下面是当前长度>=1024的逻辑
			// 新容量每次增加自身的1/4,直到超过所需容量
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// 如果溢出则新容量为所需容量
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	// 此处省略分配内存的代码
	...

	// p为新分配的底层数组的地址
	// 从old.array处拷贝lenmem个字节到p
	memmove(p, old.array, lenmem)
	// 返回新的切片
	return slice{p, old.len, newcap}
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-02-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
深度解密Go语言之Slice
Go 语言的 slice 很好用,不过也有一些坑。slice 是 Go 语言一个很重要的数据结构。网上已经有很多文章写过了,似乎没必要再写。但是每个人看问题的视角不同,写出来的东西自然也不一样。我这篇会从更底层的汇编语言去解读它。而且在我写这篇文章的过程中,发现绝大部分文章都存在一些问题,文章里会讲到,这里先不展开。
李海彬
2019/05/08
9580
深度解密Go语言之Slice
golang string和[]byte的对比
为啥string和[]byte类型转换需要一定的代价? 为啥内置函数copy会有一种特殊情况copy(dst []byte, src string) int? string和[]byte,底层都是数组
sunsky
2020/08/19
4.4K0
【Go】深入剖析slice和array
array 和 slice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是 array 的引用,相当于动态数组, 这些都是 slice 的特性,但是 slice 底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice 的情况下,怎样可以高效使用 slice? 今天借助 Go 的 unsafe 包来探索 array 和 slice 的各种奥妙。
thinkeridea
2019/11/04
4980
【Go】深入剖析slice和array
深入解析 Go 中 Slice 底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
一缕殇流化隐半边冰霜
2018/08/30
1.2K0
深入解析 Go 中 Slice 底层实现
深入解析 Go 中 Slice 底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
一缕殇流化隐半边冰霜
2024/01/27
2290
深入解析 Go 中 Slice 底层实现
深入解析 Go 中 Slice 底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
sunsky
2020/08/20
9120
深入解析 Go 中 Slice 底层实现
深入解析 Go 中 Slice 底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
李海彬
2018/07/26
9130
深入解析 Go 中 Slice 底层实现
Golang数据类型之切片
Go中的切片slice依赖于数组,它的底层就是数组,所以数组具有的优点,slice都有。 且slice支持可以通过append向slice中追加元素,长度不够时会动态扩展,通过再次slice切片,可以得到得到更小的slice结构,可以迭代、遍历等
仙人技术
2021/08/31
4150
Golang数据类型之切片
go切片
使用 make 关键字创建切片时,很多工作都需要运行时的参与;调用方必须在 make 函数中传入一个切片的大小以及可选的容量,cmd/compile/internal/gc.typecheck1 会对参数进行校验:
Michel_Rolle
2023/11/05
3K0
Go通关18:SliceHeader,slice 如何高效处理数据?
示例中变量 a1 的类型是 [1]string,变量 a2 的类型是 [2]string,因为它们大小不一致,所以不是同一类型。
微客鸟窝
2021/08/18
6401
Golang 语言中数组和切片的区别是什么?
在很多编程语言中都有数组,而切片类型却不常见。实际上,Golang 语言中的切片的底层存储也是基于数组。因为数组是固定长度的,而切片比数组更加灵活,所以在 Golang 语言中,数组使用的并不多,切片使用更加广泛。
frank.
2021/04/16
5510
Go语言切片面试真题8连问
切片是对数组的抽象,因为数组的长度是不可变的,在某些场景下使用起来就不是很方便,所以Go语言提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素。切片是一种数据结构,切片不是数组,切片描述的是一块数组,切片结构如下:
Golang梦工厂
2022/07/11
7150
Go语言切片面试真题8连问
Go数组和切片
数组是否可比较,取决于数组中的元素是否可以比较。如果数组中的元素可以比较我们可以直接通过==操作符来比较两个数组是否相等。
一行舟
2022/08/25
3520
[Golang]Go切片Silce底层实现和扩容策略(旧版)
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
宇宙无敌暴龙战士之心悦大王
2023/04/11
8950
连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。
问题 package main import ( "fmt" "reflect" "unsafe" ) func main() { var s1 []int s2 := make([]int,0) s4 := make([]int,0) fmt.Printf("s1 pointer:%+v, s2 pointer:%+v, s4 pointer:%+v, \n", *(*reflect.SliceHeader)(unsafe.Pointer(&s1)),*(*reflect.Sli
9号同学
2021/03/03
3020
连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。
切片的内部实现
type slice struct {         array unsafe.Pointer         len   int         cap   int } 第一个字段arra
李海彬
2018/03/27
1.1K0
切片的内部实现
<Go语言学习笔记>【数组与切片】
Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。
Porco1Rosso
2020/10/16
1.3K0
<Go语言学习笔记>【数组与切片】
用汇编带你看Golang里到底有没有值类型、引用类型
不管使用什么语言,日常生活中能常在技术群中看到类似这样的问题(当然这个图是我瞎编的,真实的讨论会比图中 peace 一些~):
薯条的编程修养
2022/08/10
8510
用汇编带你看Golang里到底有没有值类型、引用类型
Golang语言--slice 切片原理
golang 中的 slice 非常强大,让数组操作非常方便高效。在开发中不定长度表示的数组全部都是 slice 。但是很多同学对slice 的模糊认识,造成认为golang中的数组是引用类型,结果就
李海彬
2018/03/21
1K0
Golang语言--slice 切片原理
Go 语言切片的三种特殊状态 —— 90% 的开发者都忽视了
我们今天要来讲一个非常细节的小知识,这个知识被大多数 Go 语言的开发者无视了,它就是切片的三种特殊状态 —— 「零切片」、「空切片」和「nil 切片」。
老钱
2018/12/13
9930
相关推荐
深度解密Go语言之Slice
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验