今天我们将讨论 切片(slice) 。它们和数组非常相似,但由于切片更加灵活,在 Go 中我们会更频繁地使用切片而不是数组。
切片声明(Slice declaration)
切片的声明方式和数组很像,只是不需要指定大小:
go
var s []int
由于切片的类型中不包含长度信息 ,你可以给同一个切片变量 s 赋值不同长度的切片:
go
s = []int{0, 1, 3}
s = []int{120, 56}
s = []int{43, 42, 12, 12}
和数组一样,你可以使用 [] 来访问或修改切片中的元素:
go
s[0] = 20
fmt.Println(s[0]) // 将打印 20
切片与数组的核心区别
数组和切片的核心区别在于:
-
数组直接保存数据
-
切片 保存的是指向数组的引用(指针)
就像指针一样,切片的默认值是 nil。当切片未初始化时,也会表现出类似指针的行为。
go
var s []int // 声明了一个切片,但没有初始化;
// s 等于 nil
s[0] = 10 // 会 panic:index out of range [0] with length 0
代码解释
这里的 s 是 nil,没有底层数组,因此不能访问任何元素。
初始化切片(Initialization)
如果你需要一个包含大量元素的切片(例如 1000 个元素),该怎么办呢?
这时就需要用到 make 函数。
make 函数
make 用于分配并初始化切片(或 map、channel)。
与 new 不同:
-
new只接收类型 -
make接收 长度(len) 和 容量(cap)
make返回的是 切片本身,而不是切片的指针,因为切片已经是引用类型。
go
var s []int
s = make([]int, 6) // 如果不指定容量,cap 默认等于 len
代码解释
-
len:切片中已经存在的元素个数 -
这些元素都会被初始化为类型的零值
-
cap:在不重新分配底层数组的情况下,切片最多可以扩展到的长度
获取长度和容量
你可以使用内置函数获取切片(或数组)的长度和容量:
go
var length = len(s)
var capacity = cap(s)
数组 vs 切片(类比)
数组和切片的关系,有点类似于:
变量 vs 指针
它们的相似点包括:
-
都需要使用内置函数进行初始化
-
默认值都是
nil -
都不直接存储数据,而是引用数据
创建多维切片(Making a multi-dimensional slice)
make 只会初始化最外层切片 。
如果是 切片的切片,你需要手动初始化每一层。
下面的例子中,我们创建了一个 10×10 的二维整型切片:
go
var mds [][]int // 声明一个二维切片
mds = make([][]int, 10) // 初始化最外层切片
fmt.Println("mds =", mds) // mds = [[] [] [] [] [] [] [] [] [] []]
for i := range mds { // 遍历外层切片
mds[i] = make([]int, 10) // 初始化每一个内层切片
}
代码解释
-
make([][]int, 10)只创建了 10 个nil的[]int -
必须在循环中为每个元素单独
make([]int, 10)
如果是三维切片,就需要嵌套两层循环。
目前只需要记住这个概念即可。
切片赋值(Slice assignment)
切片在赋值时的行为非常像指针。
来看下面的例子:
go
var s = []int{12, 23, 34}
var sn = s
sn[0] = 0
sn[1] = 11
代码解释
这里的 sn 并不是 s 的独立副本 。
当你修改 sn 的元素时,s 也会被修改:
go
fmt.Println(s) // [0 11 34]
fmt.Println(sn) // [0 11 34]
这是因为:
-
切片只是指向同一个底层数组
-
var sn = s只是复制了引用,而不是数据
数组的行为不同
如果 s 是数组,那么赋值时会复制整个数据:
go
var s = [3]int{12, 23, 34} // 指定长度,变成数组
var sn = s
sn[0] = 0
sn[1] = 11
fmt.Println(s) // [12 23 34]
fmt.Println(sn) // [0 11 34]
代码解释
-
数组是值类型
-
赋值会拷贝全部元素
-
修改
sn不会影响s