切片是对数组的一种引用,Golang对于顺序存储序列的内建类型主要有两种:数组和切片。数组是定长的不会增长的,而切片是可以单向增长的。切片的实现必须依赖于数组,因为切片的底层机制就是对数组的一个引用。
builtin中对于切片类型的定义:
type slice struct {
array unsafe.Pointer // 指向数组的一个指针
len int // 当前切片的一个长度信息
cap int // 当前切片的容量信息
}
切片的创建方式
创建方式主要有:数组引用和make函数。
数组引用创建切片:
a := [5]int{0, 1, 2, 3, 4} // 创建一个大小为5的数组
s := array[0:2] // 引用array的[0,2)区间
上述代码中切片s
的数组引用a
,即结构体中array
字段指向了a
。长度字段len
的初始值等于所引用数组区间的长度,所以s
的len
属性等于2
。容量字段cap
的初始值等于所引用数组的容量,所以s
的cap
的属性等于5
。
大多数情况下,数组引用创建切片是一个坏主意。因为对切片的操作会影响原数组。具体地,对于切片的修改和追加元素(append)等操作都会影响到原数组。
s[0] = 111 // a[0] == 111 => true
s = append(s, 1111) // a[2] == 1111 => true
make函数创建切片:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
func make(t Type, size ...IntegerType) Type
正如标准库中描述的那样,使用make函数创建切片,只需要传递切片必要的length
属性和capacity
属性即可,其中第一个参数是切片的length
属性,第二个参数是切片的capacity
属性。make函数也会去创建一个底层数组供切片去引用,但是这个底层数组是用户不可见的,所以这种方式是较为推荐的。
除此之外,你也可以使用字面量的形式去创建:
s := []int{1, 2, 3, 4, 5}
使用字面量创建的切片,其容量值与长度值相等。
切片的扩容机制:二倍法。
切片的两个重要属性:len
长度属性和cap
容量属性。当切片的长度属性与容量属性相等时,若还要向切片中添加元素,则会触发切片的自动扩容机制,系统会自动申请一块新的内存,其大小为原内存块大小的二倍,然后将原切片的数据拷贝进入新的内存区域,之后返回一个新的切片。
s := []int{1, 2, 3, 4, 5}
t.Log("slice[literal] : ", s, " length : ", len(s), " capacity : ", cap(s))
//Output: [1, 2, 3, 4, 5] length : 5 capacity : 5
s = append(s, 6, 7, 8)
t.Log("slice[literal] : ", s, " length : ", len(s), " capacity : ", cap(s))
//Output: [1, 2, 3, 4, 5, 6, 7, 8] length : 8 capacity : 10
二维(多维)切片(数组)
二维数组的创建:
matrix := [3][4]int{}
fmt.Println(matrix) // [[0 0 0 0], [0 0 0 0], [0 0 0 0]]
// error: dimension must be a constant number
m, n := 3, 4
matrix := [m][n]int{}
// from literal
matrix := [3][4]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}
二维切片的创建:
// using make builtin function
matrix := make([][]int, 3)
for idx := range matrix {
matrix[idx] = make([]int, 4)
}
fmt.Println(matrix) // [[0 0 0 0], [0 0 0 0], [0 0 0 0]]
// from literal
matrix := [][]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}
和二维数组不同,二维切片的维度概念并不严格。简单地说,每行的列数不一定都是相等的,而数组每行的列数都可以确定而且相等。Golang的二维切片与Java中的二维数组很相似。
切片作为函数参数的传递
Golang语言中有指针的概念,所以对于函数而言,自然也就有了传值和传引用的区别。
基本数据类型和结构体包括数组的传递都会发生值拷贝的现象,也就是说,在函数内部操作的实际上是原变量的副本,若要对原值进行更改,那么必然需要引用的传递。
尽管Golang优先使用寄存器作为函数传递的方式,但是如果是值传递的函数调用,在寄存器中操作完毕之后的值不会写会原内存。
从切片的定义来看,它作为一个普通的结构体,在进行函数参数传递时依然发生了值拷贝,但是拷贝的值是底层数组的引用(指针),所以大多数场景下,对于切片参数即可认为是一种引用传递。
func broadcastOp(slice []int, op func(int) int) {
for idx := range slice {
slice[idx] = op(slice[idx])
}
}
s := []int{1, 2, 3, 4, 5}
broadcastOp(s, func(i int) int { return i * 2 })
t.Log("slice[param] : ", s)
//Output: [2 4 6 8 10]
但是,若要对切片的属性进行修改,那么你需要传递一个slice的指针(引用)。例如:用户想要在函数体内部向切片中添加元素(append操作)
// 方式1
func appendOp(slice []int, elements ...int) {
for _, each := range elements {
slice = append(slice, each)
}
}
// 方式2
func appendOp2(slicePtr *[]int, elements ...int) {
for _, each := range elements {
*slicePtr = append(*slicePtr, each)
}
}
// 方式3
func appendOp3(slice []int, elements ...int) []int {
for _, each := range elements {
slice = append(slice, each)
}
return slice
}
上述三种方式,只有方式2和方式3可以完成向切片中添加元素的操作。