切片是对数组的一种引用,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的初始值等于所引用数组区间的长度,所以slen属性等于2。容量字段cap的初始值等于所引用数组的容量,所以scap的属性等于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可以完成向切片中添加元素的操作。