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]


相关文章
|
1月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
38 1
|
1月前
|
数据采集 监控 Java
go语言编程学习
【11月更文挑战第3天】
42 7
|
1月前
|
Unix Linux Go
go进阶编程:Golang中的文件与文件夹操作指南
本文详细介绍了Golang中文件与文件夹的基本操作,包括读取、写入、创建、删除和遍历等。通过示例代码展示了如何使用`os`和`io/ioutil`包进行文件操作,并强调了错误处理、权限控制和路径问题的重要性。适合初学者和有经验的开发者参考。
|
2月前
|
Java 大数据 Go
Go语言:高效并发的编程新星
【10月更文挑战第21】Go语言:高效并发的编程新星
54 7
|
2月前
|
Go 数据处理 调度
Go语言中的并发模型:解锁高效并行编程的秘诀
本文将探讨Go语言中独特的并发模型及其在现代软件开发中的应用。通过深入分析 Goroutines 和 Channels,我们将揭示这一模型如何简化并行编程,提升应用性能,并改变开发者处理并发任务的方式。不同于传统多线程编程,Go的并发方法以其简洁性和高效性脱颖而出,为开发者提供了一种全新的编程范式。
|
2月前
|
存储 Go 文件存储
M.2移动硬盘打造Win To Go系统:高效分区存储文件全攻略
【10月更文挑战第12天】本文详细介绍了如何使用M.2移动硬盘制作Win To Go系统。首先,需准备合适容量与接口类型的M.2硬盘及硬盘盒,并获取Windows镜像文件和分区工具。接着,通过Rufus软件将镜像写入硬盘。文中还提供了分区策略,包括系统分区(约80-120GB)、软件分区(根据需求设定)和数据分区(剩余空间),并指导如何使用DiskGenius或Windows自带工具进行分区。最后,强调了对各分区文件的有效管理和定期备份的重要性。
178 3
|
2月前
|
存储 固态存储 Go
M.2移动硬盘打造Win To Go系统:高效分区存储文件全攻略
【10月更文挑战第11天】Win To Go 是一种将 Windows 系统安装在 M.2 移动硬盘上的技术,便于用户携带自定义系统跨设备使用。需准备高性能 M.2 硬盘及合适硬盘盒,并使用 DiskGenius 或 Rufus 进行分区与系统安装。系统分区用于安装 Windows,其余分区可根据需求存储工作或娱乐文件,便于管理和备份。
232 2
|
3月前
|
存储 缓存 Go
go语言编程系列(五)
go语言编程系列(五)
|
3月前
|
存储 JSON 安全
go语言编程系列(七)
go语言编程系列(七)
|
3月前
|
存储 安全 编译器
go语言编程系列(六)
go语言编程系列(六)