Go 编程 | 连载 12 - Slice 存储原理

简介: Go 编程 | 连载 12 - Slice 存储原理

一、Slice 存储原理

切片是基于数组实现的,切片类似一个结构体,有三个重要的组成部分,第一个是指针,指向切片实际存储数据的位置,第二个是切片的长度,第三个是切片的容量。

切片的容量始终是大于等于长度的,这样在切片添加元素的时候就不必每次重新申请一块新的内存空间存储数据。

make 方法初始化切片原理

func main() {
   alpha := make([]int, 5)
   fmt.Printf("alpha 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%v\n", alpha, len(alpha), cap(alpha))
}   
复制代码

执行上述代码,输出结果如下:

alpha 切片的内容为:[0 0 0 0 0], 长度 Len 为:5, 容量 Cap 为:5
复制代码

再看另一个例子

func main() {
   bravo := make([]int, 5, 3)
   fmt.Printf("bravo 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%v\n", bravo, len(bravo), cap(bravo))
}
复制代码

执行上述代码,输出结果如下:

# command-line-arguments
./ex9.go:10:23: invalid argument: length and capacity swapped
复制代码
func main() {
   charlie := make([]int, 5, 7)
   fmt.Printf("charlie 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%v\n", charlie, len(charlie), cap(charlie))
}
复制代码

执行上述代码,输出结果如下:

charlie 切片的内容为:[0 0 0 0 0], 长度 Len 为:5, 容量 Cap 为:6
复制代码

使用 make 函数创建切片实例时,第一个参数为类型既切片 []type,第二个参数为切片的长度既包含元素的数量,第三个参数切片的容量既切片做多可以包含元素的数量,也是切片底层的数组的长度,切片只是数组的一段截取。

从第二个例子可以看出容量是必须要大于等于长度的,只有大于长度时才能在同一片内存区域上进行添加操作,如果切片的长度等于容量,那么此时执行添加操作会重新开辟一块内存来存储数据。

func main() {
   charlie := make([]int, 5, 7)
   fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%v\n", charlie, &charlie[0], len(charlie), cap(charlie))
   charlie = append(charlie, 10)
   fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%v\n", charlie, &charlie[0], len(charlie), cap(charlie))
   charlie = append(charlie, 20)
   fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%v\n", charlie, &charlie[0], len(charlie), cap(charlie))
   charlie = append(charlie, 30)
   fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%v\n", charlie, &charlie[0], len(charlie), cap(charlie))
}
复制代码

执行上述代码,输出结果如下:

charlie 切片的内容为:[0 0 0 0 0], 内存地址为: 0xc0000b4000, 长度 Len 为:5, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10], 内存地址为: 0xc0000b4000, 长度 Len 为:6, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10 20], 内存地址为: 0xc0000b4000, 长度 Len 为:7, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10 20 30], 内存地址为: 0xc0000bc000, 长度 Len 为:8, 容量 Cap 为:14
复制代码

切片中第一个元素的内内存地址就是切片的内存地址,内存地址使用 & 获取,从输出结果来看,当切片的长度小于容量的时候,切片执行添加操作内存地址是不变的,当长度大于容量的时候,内存地址变了,说明开辟了一块新的内存空间保存数据。

image.png

如果切片的长度为 0

image.png

func main() {
   alpha := make([]int, 0)
   bravo := []int{1, 2, 3}
   fmt.Println(copy(alpha, bravo))
   fmt.Println(alpha)
   fmt.Println(bravo)
}
复制代码

执行上述代码,输出结果如下:

0
[]
[1 2 3]
复制代码

这个时候执行 copy 操作是无法完成的,因为没有内存空间存贮拷贝的内容。

截取数组获取切片原理

image.png

func main() {
   zulu := [6]int{11, 22, 33, 44, 55, 66}
   fmt.Printf("zulu 序列的类型为:%T, 长度 Len 为:%v, 容量 Cap 为:%v\n", zulu, len(zulu), cap(zulu))
   fmt.Println("数组的第二个元素的内存地址:", &zulu[1])
   yankee := zulu[1:4]
   fmt.Printf("yankee 序列的类型为:%T, 长度 Len 为:%v, 容量 Cap 为:%v\n", yankee, len(yankee), cap(yankee))
   fmt.Println("切片的第一个元素的内存地址:", &yankee[0])
   charlie := append(yankee, 55)
   fmt.Printf("%v, %v\n", len(charlie), cap(charlie))
   for idx, item := range yankee {
      fmt.Println(idx, item)
   }
}
复制代码

执行上述代码,输出结果如下:

zulu 序列的类型为:[6]int, 长度 Len 为:6, 容量 Cap 为:6
数组的第二个元素的内存地址: 0xc0000141e8
yankee 序列的类型为:[]int, 长度 Len 为:3, 容量 Cap 为:5
切片的第一个元素的内存地址: 0xc0000141e8
4, 5
0 22
1 33
2 44
复制代码

数组的第二个元素就是切片的第一个元素,内存地址相同。

切片是引用类型

func main() {
   alpha := []string{"stark", "thor", "banner"}
   bravo := alpha
   bravo[0] = strings.ToUpper(bravo[0])
   fmt.Printf("%v, %v, %v, %v\n", &bravo[0], bravo, len(bravo), cap(bravo))
   fmt.Printf("%v, %v, %v, %v\n", &alpha[0], alpha, len(alpha), cap(alpha))
}
复制代码

执行上述代码,输出结果如下:

0xc000072180, [STARK thor banner], 3, 3
0xc000072180, [STARK thor banner], 3, 3
复制代码

修改 bravo 切片时 alpha 切片的内容也被修改了,切片 alpha 和 切片 bravo 指向同一个内存地址的切片,只要其中一个对切片进行了修改,另一个变量的值也会改变。

image.png

append 返回新的切片

func main() {
   zulu := []int{1, 3, 5, 7}
   fmt.Println(&zulu[0], zulu, len(zulu), cap(zulu))
   yankee := append(zulu, 9)
   fmt.Println(&yankee[0], yankee, len(yankee), cap(yankee))
   // 修改 yankee 是否对 zulu 有影响
   yankee[0] = 10
   fmt.Println(yankee)
   fmt.Println(zulu) // 没有影响
   xray := append(yankee, 11)
   fmt.Println(&xray[0], xray, len(xray), cap(xray))
   // 修改 xray 是否对 yankee 有影响
   xray[1] = 30
   fmt.Println(xray)
   fmt.Println(yankee) // 有影响
}
复制代码

执行上述代码,输出结果如下

0xc0000b4000 [1 3 5 7] 4 4
0xc0000b8000 [1 3 5 7 9] 5 8
[10 3 5 7 9]
[1 3 5 7]
0xc0000b8000 [10 3 5 7 9 11] 6 8
[10 30 5 7 9 11]
[10 30 5 7 9]
复制代码

初始 zulu 切片的长度和容量都是 4,此时进行 append 扩容操作,Go 会新申请一块内存用来保存切片 yankee,因为原来的内存没有多余空间存储新添加的值了,此时 yankee 和 zulu 两个切片的内存地址是不同的,因此修改 yankee 不会对 zulu 有任何影响。

Go 新申请的内存保存 yankee 切片,此时 yankee 切片的长度为 5,容量为 8,还有可用空间,因此再执行 append 操作时并不会开辟新内存保存数据,而是直接在原来的内存空间上进行添加操作,此时 yankee 和 xray 两个切片执行的是同一块内存地址,有部分共享数据,所以修改 xray 会对 yankee 有影响。

但是不管是否发生扩容,append 函数总会返回一个新的切片。

那么切片在发生自动库容时,扩容机制是怎样的?

  • 如果新切片所需的最小容量大于当前切片容量的两倍,那么就直接用新切片所需的最小容量
  • 如果新切片所需的最小容量小于等于当前切片的容量的两倍
  • 如果当前切片的容量小于 1024 ,则直接把当前切片的容量翻倍作为新切片的容量
  • 如果当前切片的容量大于等于 1024 ,则每次递增切片容量的 1/4 倍,直到大于新切片所需的最小容量为止。

append 函数 与 make 函数共同使用时的陷阱

先来看一段代码

func main() {
   tango := make([]int, 3)
   whiskey := append(tango, 9)
   fmt.Println(tango)
   fmt.Println(whiskey)
}
复制代码

执行上述代码,输出结果如下:

[0 0 0]
[0 0 0 9]
复制代码

这里的输出结果 whiskey 切片的内容不是 [9] 而是 [0,0,0,9],因为 make 函数会返回一个指定长度的切片的实例,切片中元素是元素类型的默认值,并不是返回一个空的(长度为 0) 的切片。

如果想要初始化一个空的切片并通过 append 函数进行添加元素的操作,可以这么做:

func main() {
   tango := make([]int, 0)
   whiskey := append(tango, 9)
   fmt.Println(tango)
   fmt.Println(whiskey)
}
复制代码

执行上述代码,输出结果如下:

[]
[9]


相关文章
|
7月前
|
人工智能 安全 Java
Go与Java泛型原理简介
本文介绍了Go与Java泛型的实现原理。Go通过单态化为不同类型生成函数副本,提升运行效率;而Java则采用类型擦除,将泛型转为Object类型处理,保持兼容性但牺牲部分类型安全。两种机制各有优劣,适用于不同场景。
308 24
|
7月前
|
存储 人工智能 安全
深入理解 go sync.Map - 基本原理
本文介绍了 Go 语言中 `map` 在并发使用时的常见问题及其解决方案,重点对比了 `sync.Mutex`、`sync.RWMutex` 和 `sync.Map` 的性能差异及适用场景。文章指出,普通 `map` 不支持并发读写,容易引发错误;而 `sync.Map` 通过原子操作和优化设计,在某些场景下能显著提升性能。同时详细讲解了 `sync.Map` 的基本用法及其适合的应用环境,如读多写少或不同 goroutine 操作不同键的场景。
332 1
|
8月前
|
算法 Java Go
Go内存原理-GC原理
本文介绍了Go语言中垃圾回收(GC)机制的发展与实现原理,涵盖从标记-清除算法到三色标记法,再到三色标记加混合写屏障的演进过程,重点解析各版本GC的核心思想、优缺点及性能优化方向。
240 4
|
9月前
|
设计模式 缓存 算法
Go如何进行高质量编程与性能调优实践
本文介绍了Go语言高质量编程与性能调优的实践方法。高质量编程包括良好的编码习惯(如清晰注释、命名规范)、代码风格与设计(如MVC模式)、简洁明了的代码原则,以及单元测试与代码重构的重要性。性能调优方面,涵盖算法优化、数据结构选择、I/O优化、内存管理、并行与并发处理优化及代码层面的改进。通过这些方法,可有效提升代码质量和系统性能。
193 13
|
9月前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
243 10
|
9月前
|
安全 Go 开发者
Go语言之切片的原理与用法 - 《Go语言实战指南》
切片(slice)是Go语言中用于处理变长数据集合的核心结构,基于数组的轻量级抽象,具有灵活高效的特点。切片本质是一个三元组:指向底层数组的指针、长度(len)和容量(cap)。本文详细介绍了切片的声明与初始化方式、基本操作(如访问、修改、遍历)、长度与容量的区别、自动扩容机制、共享与副本处理、引用类型特性以及常见陷阱。通过理解切片的底层原理,开发者可以更高效地使用这一数据结构,优化代码性能。
340 13
|
9月前
|
人工智能 Go
[go]Slice 切片原理
本文详细介绍了Go语言中的切片(slice)数据结构,包括其定义、创建方式、扩容机制及常见操作。切片是一种动态数组,依托底层数组实现,具有灵活的扩容和传递特性。文章解析了切片的内部结构(包含指向底层数组的指针、长度和容量),并探讨了通过`make`创建切片、基于数组生成切片以及切片扩容的规则。此外,还分析了`append`函数的工作原理及其可能引发的扩容问题,以及切片拷贝时需要注意的细节。最后,通过典型面试题深入讲解了切片在函数间传递时的行为特点,帮助读者更好地理解和使用Go语言中的切片。
307 0
|
数据采集 监控 Java
go语言编程学习
【11月更文挑战第3天】
273 7
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
252 1
|
Unix Linux Go
go进阶编程:Golang中的文件与文件夹操作指南
本文详细介绍了Golang中文件与文件夹的基本操作,包括读取、写入、创建、删除和遍历等。通过示例代码展示了如何使用`os`和`io/ioutil`包进行文件操作,并强调了错误处理、权限控制和路径问题的重要性。适合初学者和有经验的开发者参考。
274 4