Go语言slice 实现原理,终于搞清楚它的扩容机制了!

简介: slice 切片,因为其可以方便的进行扩容、传递等,在实际应用中比数组更加灵活。
  1. 以下程序输出什么?
package main
import (
  "fmt"
)
func main() {
  var array [20]int
  var slice = array[10:11]
  fmt.Println("lenth: ", len(slice))
  fmt.Println("capacity: ", cap(slice))
  fmt.Println(&slice[0] == &array[10])
}
复制代码

是:

lenth:  1
capacity:  10
true
复制代码

slice 根据数组array创建,与数组共享存储空间,slice起始位置是array[5],长度为1,容量为5, slice[0]和array[5]地址相同。

  1. 以下程序输出什么?
package main
import (
 "fmt"
)
func main() {
 orderLen := 5
 order := make([]uint16, 2 * orderLen)
 pollorder := order[:orderLen:orderLen]
 lockorder := order[orderLen:][:orderLen:orderLen]
 fmt.Println("len(pollorder) = ", len(pollorder))
 fmt.Println("cap(pollorder) = ", cap(pollorder))
 fmt.Println("len(lockorder) = ", len(lockorder))
 fmt.Println("cap(lockorder) = ", cap(lockorder))
}
复制代码

是:

len(pollorder) =  5
cap(pollorder) =  5
len(lockorder) =  5
cap(lockorder) =  5
复制代码
  • order[low:high:max]意思是对 order 进行切片,新切片范围是[low, high),新切片容量是 max;
  • order 长度为2倍的 orderLen;
  • pollorder 切片是 order 的前半部分切片;
  • lockorder 是 order 的后半部分切片,即原 order 分成了两段;
  • 所以,pollorder 和 lockerorder 的长度和容量都是 orderLen,即5。

slice 实现原理

源码包 src/runtime/slice.go 定义的 slice 数据结构为:

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}
复制代码
  • array 指针指向底层数组
  • len 表示切片长度
  • cap 表示切片容量

make 创建 slice

make 创建 slice 可以同时指定其长度和容量,底层会分配一个数组,数组的长度即容量。

slice = make([]int,5,10): 表示改 slice 长度为5,容量为10。

数组创建 slice

使用数组来创建 slice 时,slice 与原数组共用一部分内存。

slice := array[5:7] 所创建的 slice ,结构如下图:

image.png

切片从数组 array[5] 开始,到数组 array[7] 结束(不包含array[7]),切片长度为 2,数组后面的内容作为切片的预留内存,即容量为5。

slice 扩容

在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。

扩容操作只只对容量,扩容后的 slice 长度不变,容量变化规则如下:

  • 若 slice 容量够用,则将新元素追加进去,slice.len++,返回原 slice
  • 若 slice 容量不够用,将 slice 先扩容,扩容得到新 slice,将新元素追加进新 slice,slice.len++,返回新 slice。

slice copy

使用 copy() 内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的的切片指向的数组中,拷贝数量取两个切片长度的最小值

例如将长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。也就是说 copy 不会发生扩容

  1. 根据数组或切片来生成新的切片一般使用 slice := array[start:end] 方式,这种新生成的切片没有指定容量,新切片的容量是从 start 开始到 array 的结束(注意并不是到 end)。
  2. 另一种写法,生成新切片同时指定其容量:slice[start:end:cap] ,其中的 cap 为新切片的容量,容量不能超过原切片实际值。

总结

  • 创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突
  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度、底层数组可用容量
  • 使用len()、cap()计算切片长度、容量时,时间复杂度均为O(1),不需要遍历切片
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而矣
  • 使用 append() 向切片追加元素时有可能触发扩容,扩容后将会生成新的切片


相关文章
|
7天前
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
53 22
零值在go语言和初始化数据
|
9天前
|
安全 算法 程序员
在go语言中使用泛型和反射
【7月更文挑战第8天】本文介绍go支持泛型后,提升了代码复用,如操作切片、映射、通道的函数,以及自定义数据结构。 泛型适用于通用数据结构和函数,减少接口使用和类型断言。
71 1
在go语言中使用泛型和反射
|
11天前
|
缓存 编译器 Shell
回顾go语言基础中一些特别的概念
【7月更文挑战第6天】本文介绍Go语言基础涵盖包声明、导入、函数、变量、语句和表达式以及注释。零值可用类型如切片、互斥锁和缓冲,支持预分配容量以优化性能。
42 2
回顾go语言基础中一些特别的概念
|
15天前
|
存储 Go API
一个go语言编码的例子
【7月更文挑战第2天】本文介绍Go语言使用Unicode字符集和UTF-8编码。Go中,`unicode/utf8`包处理编码转换,如`EncodeRune`和`DecodeRune`。`golang.org/x/text`库支持更多编码转换,如GBK到UTF-8。编码规则覆盖7位至21位的不同长度码点。
121 1
一个go语言编码的例子
|
18天前
|
JSON 算法 测试技术
在go语言中调试程序
【6月更文挑战第29天】Go语言内置`testing`包支持单元测试、基准测试和模糊测试。`go test`命令可执行测试,如`-run`选择特定测试,`-bench`运行基准测试,`-fuzz`进行模糊测试。
38 2
在go语言中调试程序
|
8天前
|
JSON Java Go
Go 语言性能优化技巧
在Go语言中优化性能涉及数字字符串转换(如用`strconv.Itoa()`代替`fmt.Sprintf()`)、避免不必要的字符串到字节切片转换、预分配切片容量、使用`strings.Builder`拼接、有效利用并发(`goroutine`和`sync.WaitGroup`)、减少内存分配、对象重用(`sync.Pool`)、无锁编程、I/O缓冲、正则预编译和选择高效的序列化方法。这些策略能显著提升代码执行效率和系统资源利用率。
43 13
|
3天前
|
Cloud Native Java Go
为什么要学习Go语言?
GO logo的核心理念,即简单胜于复杂。使用现代斜体无衬线字体与三条简单的运动线相结合,形成一个类似于快速运动的两个轮子的标记,传达速度和效率。字母的圆形暗示了GO地鼠的眼睛,创造了一个熟悉的形状,让标记和吉祥物很好地搭配在一起。
15 4
|
8天前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
41 8
|
9天前
|
前端开发 JavaScript Go
|
7天前
|
存储 Go
go语言中fmt格式化包和内置函数汇总
【7月更文挑战第10天】本文介绍fmt包和`Errorf`用于创建格式化的错误消息。`fmt`包还涉及一些接口,如`Formatter`、`GoStringer`、`ScanState`、`Scanner`和`Stringer`,支持自定义格式化和输入/输出处理。
17 1