Go-关于Slice
INFO
切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
1. Go 内置类型
go
分为基本数据类型和内置数据类型- 基本数据类型包括:
boot
byte
number
string
rune
- 内置数据类型分为:,
- 数组
- 切片
map
channel
error
2. slice
的底层实现
Slice
的本质是一个引用类型,它是对底层数组的可变长度的引用。pointer
:指向当前切片的头部length
:表示切片当中元素的数量capacity
: 表示底层数组当中可以访问的元素数量
- 这样实现的好处是什么?为什么这么设计?
- 实际上,这个设计和
Redis
的SDS(Simple Dynamic String)
比较相似 - 最根本的原因,是希望降低数组插入/删除/合并/截断操作的复杂度, 还能节省内存的分配和复制。
- 以插入元素为例,说明
slice
的工作机制 - 分成两种情况,首先,会判断
slice
的cap
容量是否充足?- 如果充足:那么,直接计算出插入的位置,执行插入之后,更新容量和长度
- 如果不足:那么,会对底层数组进行扩容(一般扩容为两倍),然后将原有数组中的元素一一拷贝至扩容后数组中(复杂度
O(n)
)。
- 总结一下,
slice
能够利用预分配的空间,避免了内存的频繁容量扩展
- 如果是合并 操作呢?也是如此,通过预分配的策略避免了内存的频繁扩展
3. 子 slice
的原理
- 在底层数组上可以创建新的
slice
结构,如sonSlice := fatherSlice[1:3]
,构造了所谓的的子slice
。 - 如果两个切片共享同一个底层数组,那么,如果一个切片修改了底层数组,另外一个切片也能感知到。
- 子
slice
的容量也可以通过偏移量计算出:子切片的容量位底层数组的长度减去切片在底层数组的偏移量。 - 当然,可以通过显式指定子切片容量的方式来规定其大小,但是不能超过底层数组限制的容量上限
go
func main() {
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 指定其容量为 4
// 但是容量不能超过底层数组原有容量
slice := source[2:3:4]
fmt.Println(cap(slice))
}
4. 切片的值传递
- 在函数当中以切片为形参的时候,本质上传递的是值,这一点和
JS
非常类似。 slice
作为形参的时候,很容易被误解为引用传递,比如下面的代码
go
package main
import "fmt"
func changeSlice(s []int) {
s[1] = 888
fmt.Printf("in func, addr: %p\n", &s)
}
func main() {
slice := []int{0, 1, 2, 3}
fmt.Printf("slice: %v %p\n", slice, &slice)
changeSlice(slice)
fmt.Printf("sclie: %v %p\n", slice, &slice)
}
- 其输出表现为:
- 可以看到,函数内部和外部的地址是不一样的,因此,一定不是引用传递。
- 那么,为什么函数内部的修改会影响外部的表现呢?因为,
go
传入的是一个数值,该数值本身的类型是slice
, 其内部保留了一个指向底层数组的指针,这个指针的值也被拷贝了一份,然后传入到函数当中了。 - 无论函数内外,访问的底层数组没有变化,其修改自然可以被函数外部的指针访问到。
- 总而言之,传递的是“值”,只不过这个值,是引用类型的值。
- 此外,如果在形参传递过程当中,发生了数组扩容,那么,形参和实参的底层数组就不是同一个数组了,其结果,自然是内部修改影响不了外部变量。
- 比如下面的代码
go
func main() {
slice := make([]int, 2, 3)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
changeSlice(slice)
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
s = append(s, 3)
s = append(s, 4)
s[1] = 111
fmt.Printf("func s: %v, addr: %p \n", s, s)
}
- 其输出如下,在
changeSlice
函数内部,底层数组扩容,形参实参指向的底层数组不一致,因此,修改无法被函数外部感知。
5. 总结:为什么需要设计出 Slice
5.1 对于操作系统而言
- 为了更好的值传递,
go
语言当中的数组无法像C/Cpp
那样作为隐式指针 参与参数的传递,在函数传参阶段,如果使用传统的数组,不可避免需要复制不必要的空间。 - 为了更好的内存分配,
go
的slice
扩容采取了类似于倍增的方式,避免了频繁的内容扩容操作,能够减少内容分配的次数。 - 为了更好的安全性,
go
会对slice
的大小进行检查,避免了底层数组的越界现象。
5.2 对于程序员而言
- 提供了一套更加简洁方便的操作接口,能够轻松产生子切片,更加容易地进行拼接/截断/插入等操作。``