在Go:数组-优快云博客一文中,我们详细讲解了数组的定义和使用,也很容易感受到在很多场景下,数组的使用都很受限,但是Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
一 切片的定义和初始化
- 切片是一种引用类型,基于数组实现,可以动态增长和收缩。
- 切片本质上是一个对底层数组的描述,包含三个部分:指向底层数组的指针、切片的长度、切片的容量。
切片定义时,我们无需指明它的长度:
// 1.直接定义一个切片
var s []int
// 2.使用:=定义并初始化切片
s4 := []int{1, 2, 3, 4}
// 3.从数组中生成切片,包含arr[1], arr[2], arr[3]
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4]
// 4.从切片s1生成一个新的切片,指向同一个底层数组
s2 := s1[:]
// 5.使用 make() 函数创建一个长度为3,容量为5的切片
s3 := make([]int, 3, 5)
二 获取切片的长度和容量
- 切片的长度是当前切片所包含的元素数目,可以通过内置的
len
函数获取。 - 容量是从切片的起始位置到底层数组末尾的元素数目,可以通过
cap
函数获取。
如图,我们可以理解为,在slice底层,有一个ptr指针指向当前切片中最后一个元素的位置,从首个元素到最后一个元素就是切片的当前所包含元素的数目,也就是len长度,而从起始位置到底层数组末尾的元素数目就是切片的容量,可以通过cap
函数获取。我们也可以使用make()函数自行指定切片的长度和容量。对于未初始化的切片,默认为 nil,长度为 0。
三 切片的截取
我们可以通过设置下限及上限(包头不包尾)来设置截取切片中的部分元素:
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int,0,5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
四 切片的扩展(含源码分析)
切片可以通过内置的append
函数动态增加元素。当追加元素时,如果切片容量不足,Go语言会自动扩展底层数组,通常会以两倍的容量扩展。
s1 = append(s1, 6, 7) // 向切片s1追加两个元素
在Go语言中,切片实际上是一个包含指向底层数组指针、长度和容量的结构体。这意味着切片的长度和容量可以独立变化,底层数组的大小则由容量决定。
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度
cap int // 切片的容量
}
我们可以看以下append
函数的核心实现部分:
func append(slice []T, elems ...T) []T {
// 获取当前切片的长度和容量
sLen := len(slice)
sCap := cap(slice)
// 如果追加元素后的长度超出当前容量,则需要扩容
if len(elems) > sCap-sLen {
// 计算新的容量
newCap := sCap
if newCap == 0 {
newCap = len(elems)
} else {
for newCap < sLen+len(elems) {
if sLen < 1024 {
newCap += newCap
} else {
newCap += newCap / 4
}
}
}
// 创建新的底层数组,并将旧数据复制到新数组
newSlice := make([]T, newCap)
copy(newSlice, slice)
slice = newSlice
}
// 将新元素追加到切片中
slice = slice[0 : sLen+len(elems)]
copy(slice[sLen:], elems)
return slice
}
append
函数首先检查当前切片的容量是否足以容纳新追加的元素。如果当前容量不足,则进入扩容逻辑。新的容量newCap
是根据当前切片的长度、容量以及追加元素的数量来计算的。也就是如果当前切片长度小于1024,新的容量将会加倍(newCap += newCap
)。如果当前切片长度大于等于1024,新容量增加为当前容量的1/4(newCap += newCap / 4
)。这种扩容策略可以使得小数组进行快速增长(加倍),而大数组则逐步增加容量,以平衡内存使用和性能。扩容后,append
函数会使用make
创建一个新的底层数组,该数组的容量为newCap
。然后使用copy
函数将旧数组的数据复制到新的底层数组中。扩容完成后,切片指向新的底层数组,并更新切片的长度以包含追加的元素。最后,将新元素复制到切片的新位置,并返回更新后的切片。
五 切片作为函数参数
切片是引用类型,多个切片可以共享同一个底层数组。对共享同一底层数组的切片进行操作时,会互相影响。因此我们在进行参数传递时,可以直接进行传递切片,而不再需要使用指针了,而且也无需特别指明切片大小。
func setS(s []int) {
s[1] = 999
fmt.Println("函数中的值:", s)
}
setS(s4)
fmt.Println("查看值是否被改变:", s4)
当我们复制数组时,和函数参数传递同理:
s5 := s4
s5[1] = 88
fmt.Println("查看值是否被改变:", s4)
如果需要独立的切片副本,可以使用copy
函数。这样我们对新切片中的元素进行修改时,九不会影响到原先的元素了。
copy(s6, s4)
s6[1] = 999
fmt.Println("查看值是否被改变:", s4)
fmt.Println("查看s6中的元素:", s6)