1.定义
表示一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装,围绕着动态数组的概念,可以按需自动增长或缩小。切片的动态增长是通过内置函数 append() 来实现的,这个函数能够快速、高效的增长切片。也可以通过对切片再次进行切割,获取更小的切片。
实际上slice是这样的结构: 创建一个特定长度和数据类型的数组,然后从这个底层数组中选取一部分元素,返回这些元素组成的集合(或容器)。换句话说,slice 维护自身的一个指针属性,指向它底层数组中的某些元素的集合。
切片三要素
地址, 长度,容量
type slice struct {
array unsafe.Pointer //指针指向底层数组的index=0
len int // 长度
cap int // 容量
}
注意:
1.slice是一个引用类型
2.创建和初始化
是否能提前知道切片的容量通常决定如何创建切片。
注意:
1.注意求字符串(string)子串操作和对字节slice([]byte)做slice操作这两者的相似性。都写作为x[m:n], 都返回原始字节的一个子序列,同时他们的底层引用方式也是相同。区别在于: 如果x是字符串, 那么 x[m:n] 返回的也是字符串; 如果x是字节slice, 那么返回的结果是字节slice。
2.1 make 来创建
make([]T, len)
make([]T, len, cap) // 和 make([]T, len)[:len] 功能相同
slice1 := make([]int, 5) // 创建一个整型切片, 其长度和容量都为5
slice2 := make([]int, 3, 5) // 创建一个字符串切片, 其长度为3, 容量为5
2.2 通过字面量来声明
初始的长度和容量会基于初始化时提供的元素的个数确定。
slice1 := []string{"a", "b", "c", "d", "e"} // 创建一个字符串切片, 其长度和容量都为5
slice2 := []int{1, 2, 3} // 创建一个整型切片, 其长度为3, 容量为5
当时用切片字面量时, 可以设置初始长度和容量。要做的就是在初始化是给出长度和容量的索引。
slice3 := []string{99: "a"} // 创建字符串切片, 使用字符串"a"初始化第100个元素
a := []int{5: 1} // 创建整型切片, 使用整型 1 初始化第6个元素
b := []string{10: "a"} // 创建字符串切片, 使用字符串"a"初始化第11个元素
c := []byte{10: 'a'} // 创建字节切片, 使用字符'a'初始化第11个元素
2.3 通过数组来创建
对已有的数组进行切片操作,得到的切片的容量为底层数组从切片的第一个元素到底层数组最后一个元素的数量。
func main() {
arr := [...]int{1, 2, 3,4, 5, 6, 7, 8, 9}
a := arr[0:]
b := arr[3:]
c := arr[:]
d := arr[2:4] // d的的第一个元素是原始元素的3个元素
fmt.Printf("a||%v||len(%v)||cap(%v)\n", a, len(a), cap(a))
fmt.Printf("b||%v||len(%v)||cap(%v)\n", b, len(b), cap(b))
fmt.Printf("c||%v||len(%v)||cap(%v)\n", c, len(c), cap(a))
fmt.Printf("d||%v||len(%v)||cap(%v)\n", d, len(d), cap(d))
}
//执行结果
a||[1 2 3 4 5 6 7 8 9]||len(9)||cap(9)
b||[4 5 6 7 8 9]||len(6)||cap(6)
c||[1 2 3 4 5 6 7 8 9]||len(9)||cap(9)
d||[3 4]||len(2)||cap(7)
2.4 切片在切割
对已有的切片再次切片操作,得到的切片的容量也遵循底层数组从切片的第一个元素到底层数组最后一个元素的数量。
func main() {
arr := [...]int{1, 2, 3,4, 5, 6, 7, 8, 9}
b := arr[3:] // 结果 [4 5 6 7 8 9]
e := b[2:4] // 结果 [6 7]
fmt.Printf("b||%v||len(%v)||cap(%v)\n", b, len(b), cap(b))
fmt.Printf("e||%v||len(%v)||cap(%v)\n", e, len(e), cap(e))
}
// 执行结果
b||[4 5 6 7 8 9]||len(6)||cap(6)
f||[6 7]||len(2)||cap(4)
2.5 完整切片表达式
对于数组, 指向数组的指针, 或切片(注意不能是字符串)支持完整切片表达式
a[low:high:max]
其中 0<=low<=high<=max<=cap(a)
上面的代码会构造和简单切片表达式 a[low:high]
相同的类型,相同长度的和元素的切片。
切片容量为 max-low
。在完整的切片表达式里,只有第一个字可以省略,其默认值为0.
low 是最低索引值,这是闭区间,也就是说第一个元素是 data 位于 low 索引处的元素;
而 high 和 max 则是开区间,表示最后一个元素只能是索引 high-1 处的元素,
而最大容量则只能是索引 max-1 处的元素。
func main() {
arr := [...]int{1, 2, 3,4, 5, 6, 7, 8, 9}
e := arr[2:4:5] // 结果 [3, 4]
fmt.Printf("e||%v||len(%v)||cap(%v)\n", e, len(e), cap(e))
}
// 执行结果
e||[3 4]||len(2)||cap(3)
2.6 切片举例
核心:
新老 slice 或者 新 slice 老数组相互影响的前提是两者共用底层数组,如果因为与执行 append 操作使得 新slice 或 老slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。
所以,问题的关键在于两者是否会共用底层数组。
举例
package main
import "fmt"
func slice_content() {
origin := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := origin[2:5]
fmt.Println("s1:", s1)
s2 := s1[2:6:7]
fmt.Println("s2:", s2)
s2 = append(s2, 100)
fmt.Println("s2:", s2)
s2 = append(s2, 100)
fmt.Println("s2:", s2)
fmt.Println("s1:", s1)
s1[2] = 20
fmt.Println("s1:", s1)
fmt.Println("s2:", s2)
fmt.Println("origin:", origin)
}
func main() {
slice_content()
}
/*
返回结果
s1: [2 3 4]
s2: [4 5 6 7]
s2: [4 5 6 7 100]
s2: [4 5 6 7 100 100]
s1: [2 3 4]
s1: [2 3 20]
s2: [4 5 6 7 100 100]
origin: [0 1 2 3 20 5 6 7 100 9]
*/
s2 第二次追加 100 时, s2 的容量不够用,需要进行扩容。
于是 s2 将原来的元素复制到新的位置,扩大自己的容量。并且为了应对未来的可能的append带来的再一次扩容,
s2 会在此次扩容的时候多留一些 buffer, 将新的容量扩大为原始容量的2倍,也就是10。
index | 表达式 | len | cap | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
origin | 10 | 10 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
s1 | s1:=origin[2:5] | 3 | 8 | 2 | 3 | 4 | |||||||
s2 | s2:=s1[2:6:7] low = 2 high = 6 max = 7 |
4 | 5 | 4 | 5 | 6 | 7 | ||||||
s2 = append(s2, 100) | |||||||||||||
origin | 10 | 10 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 100 | 9 | |
s1 | 3 | 8 | 2 | 3 | 4 | 100 | |||||||
s2 | 5 | 5 | 4 | 5 | 6 | 7 | 100 | ||||||
s2=append(s2, 100) | |||||||||||||
origin | 10 | 10 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 100 | 9 | |
s1 | 3 | 8 | 2 | 3 | 4 | 100 | |||||||
s2 | 6 | 10 | 4 | 5 | 6 | 7 | 100 | 100 | |||||
s1[2] = 20 | |||||||||||||
origin | 10 | 10 | 0 | 1 | 2 | 3 | 20 | 5 | 6 | 7 | 100 | 9 | |
s1 | 3 | 8 | 2 | 3 | 20 | 100 | |||||||
s2 | 6 | 10 | 4 | 5 | 6 | 7 | 100 | 100 |
3.切片比较
1.切片之间不能直接比较,我们不能使用==
操作符来判断两个切片是否含有全部相等元素。
因为切片是引用类型, 它的元素是非直接的,如果底层数组元素改变,同一个slice在不同时间会拥有不同的元素。
切片可以和nil比较。
2.一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是 nil
3.判断一个切片是否为空, 应该用 len(s)==0
, 而不能用 s==nil
func main() {
var nil_s []int
empty_s := []int{}
println(nil_s) // [0/0]0x0
println(empty_s) // [0/0]0xc000078d80
println(nil_s == nil) // true
println(empty_s == nil) // false
}
4.赋值
切片是引用类型,当一个切片赋值给另一个切片变量时,两者公用一套底层数组,对一个切片的影响会影响另一个切片的内容。
5.拷贝
使用Go的内置函数 copy() 进行拷贝func copy(dst, src []Type) int
它表示把切片 src 中的元素拷贝到切片 dst 中,返回值为拷贝成功的元素个数。如果 src 比 dst 长,就截断;如果 src 比 dst 短,则只拷贝 src 那部分。
func main() {
var a = []int{1, 2, 3, 4, 5, 6}
b := make([]int, 3)
n1 := copy(b, a)
fmt.Println("n1:", n1)
fmt.Printf("b:%v", b)
}
//执行结果
n1: 3
b:[1 2 3]
6.切片增长
切片增长可以通过 Golang 的内置函数 append() 来操作。func append(slice []Type, elems ...Type) []Type
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
7.切片删除
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
8.遍历
和数组类似,有两种遍历方式。
当时用 range 方式时, 拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变, 如图。
由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。
func main() {
var a = []string{"a", "b", "c", "d", "e", "f"}
for i := 0; i < len(a); i++ {
fmt.Printf("(%d, %v)\n", i, a[i])
}
fmt.Println("--------------------")
for index, value := range a {
fmt.Printf("value=%v, value_addr=%p, slice_value_addr=%p\n", value, &value, &a[index])
}
fmt.Println("--------------------")
for _, value := range a {
value = value + "1" // 直接修改 value 值是无效的
}
for index, value := range a {
fmt.Printf("value=%v, value_addr=%p, slice_value_addr=%p\n", value, &value, &a[index])
}
fmt.Println("--------------------")
for index, value := range a {
a[index] = value + "1"
}
for index, value := range a {
fmt.Printf("value=%v, value_addr=%p, slice_value_addr=%p\n", value, &value, &a[index])
}
}
// 执行结果
(0, a)
(1, b)
(2, c)
(3, d)
(4, e)
(5, f)
--------------------
value=a, value_addr=0xc00000e240, slice_value_addr=0xc00005a180
value=b, value_addr=0xc00000e240, slice_value_addr=0xc00005a190
value=c, value_addr=0xc00000e240, slice_value_addr=0xc00005a1a0
value=d, value_addr=0xc00000e240, slice_value_addr=0xc00005a1b0
value=e, value_addr=0xc00000e240, slice_value_addr=0xc00005a1c0
value=f, value_addr=0xc00000e240, slice_value_addr=0xc00005a1d0
--------------------
value=a, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a180
value=b, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a190
value=c, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a1a0
value=d, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a1b0
value=e, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a1c0
value=f, value_addr=0xc00000e2b0, slice_value_addr=0xc00005a1d0
--------------------
value=a1, value_addr=0xc00000e320, slice_value_addr=0xc00005a180
value=b1, value_addr=0xc00000e320, slice_value_addr=0xc00005a190
value=c1, value_addr=0xc00000e320, slice_value_addr=0xc00005a1a0
value=d1, value_addr=0xc00000e320, slice_value_addr=0xc00005a1b0
value=e1, value_addr=0xc00000e320, slice_value_addr=0xc00005a1c0
value=f1, value_addr=0xc00000e320, slice_value_addr=0xc00005a1d0
9.在函数将传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复 制和传递切片成本也很低。
在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量 字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片 复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底 层数组。
// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}
9.1 切片作为函数参数会改变吗?
需要说明的是,Go 语言中的函数参数传递,只有值传递,没有引用传递。
切片是一个结构体,包含了是三个成员:array, len, cap, 即 底层数组的地址,切片长度,容量。
当 slice 作为函数参数时,就是一个普通的结构体。从这个角度其实很好理解:若直接传slice,在调用者看来,实参 slice 并不会被函数中对形参的操作改变,
实参是形参的一个复制;若传的是 slice的指针,则会影响实参。
需要注意的是,不论传的 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,都会反映到实参 slice 的底层数据。
10.代码片段
10.1 slice 反转
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
append 源码解析 和 扩展原理
//TODO
参考资料
《Go in action》
《The Go Programming Language》
https://www.liwenzhou.com/posts/Go/06_slice/
https://halfrost.com/go_slice/
《Go 程序员 面试笔试宝典》