引用类型的在内存栈空间中存储的是保存变量名和指向堆空间中的变量地址,地址指向的堆空间中保存着实际的值。在赋值或拷贝变量时,栈空间中保存的地址也被拷贝指向相同的堆空间中保存的值,所以在修改其中一个变量的值时,其他的变量会一起被修改。
类型 | 默认值 | 说明 |
---|---|---|
slice | nil | 引用类型 |
map | nil | 引用类型 |
channel | nil | 引用类型 |
01 Slice 切片简介
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型,类似于 C/C++ 中的数组类型,而Golang 中的数组是值类型,赋值和函数传参操作都会复制整个数组数据。
相较于 Golang 数组拷贝过程中的巨大内存开销,采用切片的方式进行赋值或者传参不需要使用额外的内存并且比使用数组更有效率。
切片的实现原理类似 C++ STL 中的 vector,但是切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
type slice struct {
array unsafe.Pointer // 保存指向堆空间的地址
len int // 包含元素的个数(实际被使用容量)长度
cap int // 切片的实际容量(使用部分+未使用部分)通常大于等于len
}
02 创建切片
声明切片的方式与数组声明类似,但是不在[]
中指明长度,数组是固定长度的,而切片的长度可变的。
// 数组声明
var arr1 [3]int
var arr2 [...]int{1,2,3}
arr3 := [3]int{1,2,3} // 短变量声明
// 切片声明
var s1 []int // 空切片
s1 := []int{1,2,3} // 短变量声明
make函数初始化切片:Golang 的 make 内置函数用于分配内存空间,返回引用类型本身。make 函数有三个入参分别是:数据类型(*_type)
,长度(len)
和容量(cap)
,如果容量被省略则与长度同值。
// 声明形式
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap)
// example:
func SliceCreate() {
var s1 []int = make([]int, 3, 5)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
var s2 []int = make([]int, 3)
fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
}
/* output:
s1: [0 0 0] len / cap: 3 / 5
s2: [0 0 0] len / cap: 3 / 3
*/
空切片初始化:nil切片表示该切片结构体的指针指向nil
,表示切片不存在,常用于函数异常返回值。空切片的指针指向具体地址但该地址没有存放任何元素,空切片一般会用来表示一个空的集合。
func SliceCreate() {
var s3 []int // nil切片
if s3 == nil {
fmt.Println("s3 is empty")
}
s3 = []int{1, 2, 3}
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
// 空切片
s4 := make([]int, 0)
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
s4 = []int{}
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
s4 = []int{1, 2, 3, 4}
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
}
/* output:
s3 is empty
s3: [1 2 3] len / cap: 3 / 3
s4: [] len / cap: 0 / 0
s4: [] len / cap: 0 / 0
s4: [1 2 3 4] len / cap: 4 / 4
*/
使用字面量从数组中切片:使用切片引用数组连续的全部或部分数据,使用字面量(索引号)获取数组的部分数据,字面量操作含义如下表所示
操作 | 含义 |
---|---|
arr[n] | 索引号为n 的单个元素 |
arr[:] | 从索引位置0到len(arr)-1 中所获得的切片即数组的所有元素 |
arr[low:] | 从索引位置low 到len(arr)-1 中所获得的切片,长度为len(arr)-low ,容量为len(arr) |
arr[:high] | 从索引位置0到high-1 中所获得的切片,长度为high ,容量为len(arr) |
arr[low:high] | 从索引位置low 到high-1 中所获得的切片,长度为high-low ,容量为len(arr) |
arr[low:high:max] | 从索引位置low 到high-1 中所获得的切片,长度为high-low ,容量为max-low |
func SliceCreate() {
var arr = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
s5 := arr[1:5:6]
fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
s6 := arr[1:5]
fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}
/* output:
s5: [2 3 4 5] len / cap: 4 / 5
s6: [2 3 4 5] len / cap: 4 / 9
*/
03 操作切片
3.1 切片追加
append()
内置函数添加元素:切片使用append()
内置函数向该切片末尾追加元素,并返回新的切片。
func SliceAppend() {
s1 := make([]int, 1)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
s2 := append(s1, 1)
fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
fmt.Printf("pos s1: %p; pos s2: %p\n", &s1, &s2)
}
/* output:
s1: [0] len / cap: 1 / 1
s2: [0 1] len / cap: 2 / 2
pos s1: 0xc0000040d8; pos s2: 0xc000004108
*/
向切片中追加多个元素:
- 切片追加多个元素,可以通过多次调用
append()
,也可以添加多个入参
func SliceAppend() {
s3 := []int{1, 2, 3}
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
s3 = append(s3, 4)
s3 = append(s3, 5)
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
s3 = append(s3, 6, 7, 8)
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
}
/* output:
s3: [1 2 3] len / cap: 3 / 3
s3: [1 2 3 4 5] len / cap: 5 / 6
s3: [1 2 3 4 5 6 7 8] len / cap: 8 / 12
*/
- 使用
append()
将切片作为追加元素,使用...
运算符将切片值拆分成单个追加元素
func SliceAppend() {
s4 := []int{1, 2, 3}
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
s5 := []int{4, 5}
fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
s6 := append(s4, s5...)
fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}
/* output:
s4: [1 2 3] len / cap: 3 / 3
s5: [4 5] len / cap: 2 / 2
s6: [1 2 3 4 5] len / cap: 5 / 6
*/
3.2 拷贝切片
copy()
内置函数拷贝切片:使用 copy 内置函数拷贝切片时,是将切片的数据拷贝到另外新开辟的内存空间中;copy 内置函数的参数和返回值为copy( dest Slice, src Slice []T) int
,其中第一个参数为拷贝的目标切片,第二个参数是拷贝的对象即数据源,返回值表示的是根据两个切片长度len
的较小值实际成功拷贝的元素个数。
func SliceCopy() {
s1 := make([]int, 3, 5)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
s2 := []int{1, 2}
fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
copy(s1, s2)
fmt.Println("copy s1:", s1, " len / cap:", len(s1), "/", cap(s1))
fmt.Println("copy s2:", s2, " len / cap:", len(s2), "/", cap(s2))
s3 := []int{3}
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
s1 = append(s1, s3...)
fmt.Println("append s1:", s1, " len / cap:", len(s1), "/", cap(s1))
copy(s3, s2)
fmt.Println("copy s3:", s3, " len / cap:", len(s3), "/", cap(s3))
fmt.Println("copy s2:", s2, " len / cap:", len(s2), "/", cap(s2))
}
/* output:
s1: [0 0 0] len / cap: 3 / 5
s2: [1 2] len / cap: 2 / 2
copy s1: [1 2 0] len / cap: 3 / 5
copy s2: [1 2] len / cap: 2 / 2
s3: [3] len / cap: 1 / 1
append s1: [1 2 0 3] len / cap: 4 / 5
copy s3: [1] len / cap: 1 / 1
copy s2: [1 2] len / cap: 2 / 2
*/
=
赋值运算符浅拷贝:Golang 中有了 Array 数组还提出 Slice 切片的一个重要动机就是当数组保存数据规模过大时,避免全部重新复制一遍数组元素,而使用指向存储实际数据空间的指针高效利用内存。使用=
拷贝切片时,两者引用同一个内存空间,当修改其中一个时,两者同时被修改。
func SliceCopy() {
s4 := []int{1, 2, 3}
s5 := []int{}
s6 := make([]int, 3)
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
s5 = s4
copy(s6, s4)
fmt.Println("copy s4:", s4, " len / cap:", len(s4), "/", cap(s4))
fmt.Println("copy s5:", s5, " len / cap:", len(s5), "/", cap(s5))
fmt.Println("copy s6:", s6, " len / cap:", len(s6), "/", cap(s6))
s4[0] = 9
fmt.Println("modify s4:", s4, " len / cap:", len(s4), "/", cap(s4))
fmt.Println("modify s5:", s5, " len / cap:", len(s5), "/", cap(s5))
fmt.Println("modify s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}
/* output:
s4: [1 2 3] len / cap: 3 / 3
s5: [] len / cap: 0 / 0
s6: [0 0 0] len / cap: 3 / 3
copy s4: [1 2 3] len / cap: 3 / 3
copy s5: [1 2 3] len / cap: 3 / 3
copy s6: [1 2 3] len / cap: 3 / 3
modify s4: [9 2 3] len / cap: 3 / 3
modify s5: [9 2 3] len / cap: 3 / 3
modify s6: [1 2 3] len / cap: 3 / 3
*/
3.3 遍历切片
for i/for range
遍历切片:和遍历数组一样可以使用标准索引法遍历切片,使用len()
内置函数获取数组长度,然后使用[]
中括号运算符获取索引元素;也可以使用for range
这种更加便捷的遍历方式遍历引用。
func SliceTraversal() {
s1 := []int{6, 5, 4, 3, 2, 1}
for i := 0; i < len(s1); i++ {
fmt.Printf("index: %d, value: %d\n", i, s1[i])
}
fmt.Println("--------------")
for index, value := range s1 {
fmt.Printf("index: %d, value: %d\n", index, value)
}
}
/* output:
index: 0, value: 6
index: 1, value: 5
index: 2, value: 4
index: 3, value: 3
index: 4, value: 2
index: 5, value: 1
--------------
index: 0, value: 6
index: 1, value: 5
index: 2, value: 4
index: 3, value: 3
index: 4, value: 2
index: 5, value: 1
*/
3.4 切片删除
Golang 中切片元素的删除过程并没有提供任何的语法糖或者方法封装,删除元素需要以被删除元素为分界点,将前后两个部分的内存重新连接起来。但这种方法在切片数据规模较大时非常低效。
func SliceDelete() {
s1 := []int{0, 1, 2, 3, 4, 5}
index := 3
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
fmt.Println("before: ", s1[:index], "after: ", s1[index+1:])
s1 = append(s1[:index], s1[index+1:]...)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
}
/* output:
s1: [0 1 2 3 4 5] len / cap: 6 / 6
before: [0 1 2] after: [4 5]
s1: [0 1 2 4 5] len / cap: 5 / 6
*/
04 切片扩容
前面介绍到 Golang 切片的实现原理类似 C++ STL 中的 vector,切片中也有类似于 vector 中动态扩容的智能动作。
当切片使用append()
内置函数追加元素时,如果当前切片容量cap
被使用完时,就需要重新开辟一块新的内存空间,然后把原数据拷贝到该新空间中,并把指向原地址空间的切片指针重定向到新空间,最后释放掉原存储数据的空间。
Go 中切片扩容的策略:按照 2 倍或者 1.5 倍扩大原切片的容量cap
(注意不是长度len
)。
-
如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量,即每次增加原来容量的一倍
-
一旦元素个数超过 1024 个元素,那么增长因子就变成 1.5 ,即每次增加原来容量的四分之一
func SliceExpend() {
s1 := make([]int, 3, 5)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
s1 = append(s1, 4, 5, 6)
fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
s2 := make([]int, 1023, 1024)
fmt.Println("ori s2:", " len / cap:", len(s2), "/", cap(s2))
s2 = append(s2, 1024, 1025)
fmt.Println("exp s2:", " len / cap:", len(s2), "/", cap(s2))
}
/* output:
s1: [0 0 0] len / cap: 3 / 5
s1: [0 0 0 4 5 6] len / cap: 6 / 10
ori s2: len / cap: 1023 / 1024
exp s2: len / cap: 1025 / 1536
*/
不触发切片自动扩容的情况:Golang 切片扩容机制中要注意如果切片创建是通过字面量对 Array 数组的截取,要注意明确第三个参数 cap 值,当 cap 并不等于指向数组的总容量时且切片长度小于容量时,不会触发自动扩容,导致切片指针指向的就是原 Array 数组,当数组元素发生改变时也会影响切片。
所以用字面量创建切片的时候,cap 的值一定要明确,避免共享原数组导致的 bug。
func SliceExpend() {
arr := [5]int{1, 2, 3, 4, 5}
s3 := arr[:3]
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
s3 = append(s3, 4)
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
arr[0] = 9
fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
}
/* output:
s3: [1 2 3] len / cap: 3 / 5
s3: [1 2 3 4] len / cap: 4 / 5
s3: [9 2 3 4] len / cap: 4 / 5
*/
切片扩容导致的索引失效:当切片作为函数参数时,如果在函数内部发生了扩容,这时再修改切片中的值不会生效,因为修改发生在新开辟的内存空间中,对原先的数据没有任何影响。
func SliceExpend() {
s4 := make([]int, 3, 4)
fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
appendToExpend(s4)
fmt.Println("append s4:", s4, " len / cap:", len(s4), "/", cap(s4))
s5 := []int{1, 2, 3}
fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
appendToExpend(s5)
fmt.Println("append s5:", s5, " len / cap:", len(s5), "/", cap(s5))
}
func appendToExpend(s []int) {
s = append(s, 4)
s[0] = 9
}
/* output:
s4: [0 0 0] len / cap: 3 / 4
append s4: [9 0 0] len / cap: 3 / 4
s5: [1 2 3] len / cap: 3 / 3
append s5: [1 2 3] len / cap: 3 / 3
*/