1. 切片定义
GoGo
slice
SliceHeadergo
type SliceHeader struct {
array unsafe.Pointer
len int
cap int
}
所有切片的大小相同;
arraylencapcaplen
slice
我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
Go
声明一个未指定大小的数组来定义切片:
var varName []type
make()
var varName []type = make([]type, len)
// 也可以简写为
varName := make([]type, len)
也可以指定容量,其中 capacity 为可选参数。
make([]type, length, capacity)
typelengthcapacitycaplength
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b) // [0 0] [0 0]
fmt.Println(len(a), len(b)) // 2 2
其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。
make()
Go
2. 切片初始化
- 直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3, 其 cap=len=3
s := []int{1,2,3 }
- 切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。初始化切片 s,是数组 arr 的引用
s := arr[:]
- 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
从数组生成切片,代码如下:
var a = [3]int{1, 2, 3}
fmt.Println(a, a[1:2]) // [1 2 3] [2]
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]
基于数组创建的切片,它的起始元素从 low 所标识的下标值开始,切片的长度(len)是 high - low,它的容量是 max - low。而且,由于切片 sl 的底层数组就是数组 arr,对切片 sl 中元素的修改将直接影响数组 arr 变量。比如,如果我们将切片的第一个元素加 10,那么数组 arr 的第四个元素将变为 14:
sl[0] += 10
fmt.Println("arr[3] =", arr[3]) // 14
在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色。切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销。因为我们传递的并不是数组本身,而是数组的“描述符”,而这个描述符的大小是固定的(见上面的三元组结构),无论底层的数组有多大,切片打开的“窗口”长度有多长,它都是不变的。此外,我们在进行数组切片化的时候,通常省略 max,而 max 的默认值为数组的长度。
- 缺省 endIndex 时将表示一直到 arr 的最后一个元素
s := arr[startIndex:]
- 缺省 startIndex 时将表示从 arr 的第一个元素开始
s := arr[:endIndex]
- 通过切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
- 通过内置函数 make() 初始化切片 s,[]int 标识为其元素类型为 int 的切片,由 make 创建的切片各元素默认为该类型零值。
s :=make([]int, len, cap)
从数组或切片生成新的切片拥有如下特性:
-
取出的元素数量为:结束位置 - 开始位置;
-
取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
-
当缺省开始位置时,表示从连续区域开头到结束位置;
-
当缺省结束位置时,表示从开始位置到整个连续区域末尾;
-
两者同时缺省时,与切片本身等效;
a := []int{1, 2, 3}
fmt.Println(a[:]) // [1 2 3]
a3aa
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
a := []int{1, 2, 3}
fmt.Println(a[0:0]) // []
slicelen(slice)
3. 数组和切片声明差异
数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。
[]
// 创建有3个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是3的整型切片
slice := []int{10, 20, 30}
4. 字符串和切片转换
s := "hello"
a := []byte(s) // 将字符串转换为 byte 类型切片
b := []rune(s) // 将字符串转换为 rune 类型切片
5. len() 和 cap() 函数
数组的容量永远等于其长度,都是不可变的。
len()
cap()
package main
import "fmt"
func main() {
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
输出结果为:
len=3 cap=5 slice=[0 0 0]
示例:
func main() {
s1 := make([]int, 5)
// 用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致
fmt.Printf("The length of s1: %d\n", len(s1)) // 5
fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
fmt.Printf("The value of s1: %d\n", s1) // [0 0 0 0 0]
s2 := make([]int, 5, 8)
fmt.Printf("The length of s2: %d\n", len(s2)) // 5
fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 8
fmt.Printf("The value of s2: %d\n", s2) // [0 0 0 0 0]
}
当我们通过切片表达式基于某个数组或切片生成新切片的时候,如下
func main() {
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
fmt.Printf("The length of s4: %d\n", len(s4)) // 3
fmt.Printf("The capacity of s4: %d\n", cap(s4)) // 5
fmt.Printf("The value of s4: %d\n", s4) // [4 5 6]
}
make
更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。由于 s4 是通过在 s3 上施加切片操作得来的,所以 s3 的底层数组就是 s4 的底层数组。又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,s4 的容量就是其底层数组的长度 8 减去上述切片表达式中的那个起始索引 3,即 5。
s5 := s4[:cap(s4)]
fmt.Printf("The length of s5: %d\n", len(s5)) // 5
fmt.Printf("The capacity of s5: %d\n", cap(s5)) // 5
fmt.Printf("The value of s5: %#v\n", s5) // []int{4, 5, 6, 7, 8}
6. 空切片与 nil 切片
nilnilnil
// 创建nil整型切片
var varName []int
nil
利用初始化,通过声明一个切片可以创建一个空切片
// 使用make创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}
nilappendlencap
实例如下:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
输出结果为:
len=0 cap=0 slice=[]
切片是空的
nil
var s1 []int
var s2 = []int{}
s1nillencaparraynilniltrues2nilnilfalse
更推荐第一种写法
package main
import "fmt"
func main() {
var sl1 []int
var sl2 = []int{}
fmt.Printf("%T, %v, %p\n", sl1, sl1, sl1) // []int, [], 0x0
fmt.Printf("%T, %v, %p\n", sl2, sl2, sl2) // []int, [], 某个地址值
fmt.Println(sl1 == nil) // true
fmt.Println(sl2 == nil) // false
fmt.Println(len(sl1), cap(sl1)) // 0, 0
fmt.Println(len(sl2), cap(sl2)) // 0, 0
// fmt.Println(sl1[0]) 下标越界 panic
// fmt.Println(sl2[0]) 下标越界 panic
sl1 = append(sl1, 1) // 可以 append 操作
sl2 = append(sl2, 1) // 可以 append 操作
}
7. 切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound],实例如下:
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)
}
8. append() 函数
Goappend()append()
切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……
Go
Go
另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。
package main
import "fmt"
func main() {
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包
fmt.Println(a)
var numbers []int
for i := 0; i < 5; i++ {
numbers = append(numbers, i)
fmt.Printf("len: %d cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
}
}
输出结果:
[1 1 2 3 1 2 3]
len: 1 cap: 1 pointer: 0xc0000180d0
len: 2 cap: 2 pointer: 0xc0000180f0
len: 3 cap: 4 pointer: 0xc0000141a0
len: 4 cap: 4 pointer: 0xc0000141a0
len: 5 cap: 8 pointer: 0xc00001a180
从内存地址可以看出:
append
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
appendappend
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
appenda[i:]a[:i]
package main
import "fmt"
func main() {
var a = []int{1, 2, 3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3, -2, -1}, a...) // 在开头添加1个切片
fmt.Println(a) // [-3 -2 -1 0 1 2 3]
var b []int
b = append(a[:0], append([]int{10}, b[0:]...)...) // 在第0个位置插入10
b = append(a[:0], append([]int{1, 2, 3}, b[0:]...)...) // 在第0个位置插入切片
fmt.Println(b) // [1 2 3 10]
}
问题 2:切片的底层数组什么时候会被替换?
Go
appendappend
append
基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中了。
9. copy() 函数
Gocopy()
copy()
copy(destSlice, srcSlice) int
srcSlicedestSlicesrcSlicedestSlicecopy()
package main
import "fmt"
func main() {
a := []int{10, 20, 30, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, a) // 只会复制 a 的前3个元素到slice2中
fmt.Println(slice2) // [10 20 30]
slice2 = []int{5, 4, 3}
b := []int{0, 0, 0, 0, 0}
ret := copy(b, slice2) // 只会复制slice2的3个元素到b 的前3个位置
fmt.Println(b) // [5 4 3 0 0]
fmt.Println("ret is ", ret) // ret is 3
}
9.1 切片索引生成的切片与原来的切片是同一个地址
如果多个切片指向同一底层数组,那么对其中一个切片的改变影响其它的切片。
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[0:3] // 只是基于同一个底层数组生成了一个新的切片(或者说窗口)
// s2 := make([]int, 3)
copy(s2, s1)
s2 = append(s2, 40)
s1[2] = 30
fmt.Printf("The length of s1: %d\n", len(s1)) // 5
fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
fmt.Printf("The value of s1: %d\n", s1) // [1 2 30 40 5]
fmt.Printf("The length of s2: %d\n", len(s2)) // 4
fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 5
fmt.Printf("The value of s2: %d\n", s2) // [1 2 30 40]
fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a330
fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}
9.2 使用make 会生成新的切片
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3, 4, 5}
// s2 := s1[0:3]
s2 := make([]int, 3)
copy(s2, s1)
s2 = append(s2, 40)
s1[2] = 30
fmt.Printf("The length of s1: %d\n", len(s1)) // 5
fmt.Printf("The capacity of s1: %d\n", cap(s1)) // 5
fmt.Printf("The value of s1: %d\n", s1) // [1 2 30 4 5]
fmt.Printf("The length of s2: %d\n", len(s2)) // 4
fmt.Printf("The capacity of s2: %d\n", cap(s2)) // 6
fmt.Printf("The value of s2: %d\n", s2) // [1 2 3 40]
fmt.Printf("The address of s1: %p\n", s1) // 0xc00001a300
fmt.Printf("The address of s2: %p\n", s2) // 0xc00001a330
}
10. 切片删除元素
Go
10.1 从开头位置删除
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
还可以用 copy() 函数来删除开头的元素:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
10.2 从中间位置删除
appendcopy
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
10.3 从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
注意:
连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。