切片(slice)定义
go语言中的slice是一种数据结构,其定义为一个结构体,如下所示;
type SliceHeader struct {
Data uintptr // 指向底层数组的指针
Len int // 切片的长度
Cap int // 切片的容量
}
切片与数组
- 切片的底层数据存储结构是 数组
- 切片较为灵活,能动态扩容,而数组是定长的,长度确定后无法更改;
- 数组中的 数据长度是数组类型的一部分; 在参数定义传参时较为局限;而 切片没有此问题;
切片与底层数组的关系及影响
- 根据切片的结构体可以看出 切片的数据存储在数组上,而切片和底层数组之间是用 指针相关联;
- 就是因为 这个指针关联 的特性会造成 切片间数据的相互影响,因为其操作的都是同一个底层数组;
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4}
s1 := s[1:3]
fmt.Println(s, s1) // [1 2 3 4] [2 3]
s1[0] = 22
fmt.Println(s, s1) //s1的操作影响到了s的结果 [1 22 3 4] [22 3]
s[2] = 33
fmt.Println(s, s1) //s的操作影响到了s1的结果 [1 22 33 4] [22 33]
}
切片的扩容过程及切片间的影响
- 切片的扩容发生在底层数组长度不够存放新数据时,才触发扩容,每次扩容大小的机制在go语言不同版本有所不同,基本 都是先扩容到原来的2倍(原容量不大时,较大时扩容倍数小些),然后再进行内存对齐后获得最终的扩容大小;
- 扩容会额外预分配一定的空间,防止每次新增数据都触发扩容, 每次内存的copy太消耗资源;
- 扩容 过程为 新建一个计算好大小的数组,将原切片中的数据(不是原数组的数据)复制到新数组中,然后将这次新增的数据添加进去,再 将新数组的地址赋值给 切片结构体的 数组指针字段;
- 基于如上扩容特点,可知 扩容后 切片间的操作不再互相影响,如下示例所示;
package main
import (
"fmt"
"reflect"
"unsafe"
)
//
// @Description: 获取切片对应底层数组的指针
// @param s:
// @return unsafe.Pointer:
//
func BottomLayerArrayPoint(s *[]int) unsafe.Pointer {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(s))
return unsafe.Pointer(hdr.Data)
}
func main() {
s := []int{1, 2, 3, 4}
s1 := s[1:3]
fmt.Println(s, s1) // [1 2 3 4] [2 3]
s1[0] = 22
fmt.Println(s, s1) //s1的操作影响到了s的结果 [1 22 3 4] [22 3]
s[2] = 33
fmt.Println(s, s1) //s的操作影响到了s1的结果 [1 22 33 4] [22 33]
fmt.Println("扩容前底层数组的容量", cap(s1), "s1:", s1, "s1底层数组的指针", BottomLayerArrayPoint(&s1), "s1底层数组的值", *(*[3]int)(BottomLayerArrayPoint(&s1)))
// 对s1新增数据,底层数组长度不够,触发扩容,创建新的数组,并将s1的数组指针指向新的数组,所以 后续 对 s1的操作不会再影响到s;因为2者的底层数据不同了;
s1 = append(s1, 5, 6)
fmt.Println("扩容后底层数组的容量", cap(s1), "s1:", s1, "s1底层数组的指针", BottomLayerArrayPoint(&s1), "s1底层数组的值", *(*[6]int)(BottomLayerArrayPoint(&s1)))
s1[0] = 222
// 发现s1的操作不会再影响到s;
fmt.Println(s, s1)
}
append操作时切片和底层数组的执行过程
append函数分析
func append(slice []Type, elems ...Type) []Type
- 特别注意: 此函数入参有一个slice,出参返回一个新的slice,也就是 这个新的slice的结构体中的 指向底层数组的指针可能会换成新的,且 容量,长度字段都可能会变化;
- 另外, append入参slice是 值传递,也就是 函数内部 会新建一个slice结构体,只是这个结构体每个字段的值和入参slice相同而已,但是 也因为这个特点 导致 执行底层数组的指针也相同,导致 append操作可能会操作相同的底层数组(除非触发扩容);
- 至于 将出参返回的新slice赋值给 入参切片变量(
s=append(s,1)
) 还是新的切片变量(s1=append(s,1)
),就会产生不同的结果;
append是否触发扩容的不同结果
- 触发扩容时, 新建的数组与 原数组没任何关系
- 未触发扩容时,对原数组的操作 会影响其他切片
是否扩容的示例代码
package main
import "fmt"
func main() {
s := []int{5, 7}
//s := []int{5}
fmt.Println("s值", s, "s容量", cap(s))
s = append(s, 9)
fmt.Println("s值", s, "s容量", cap(s))
y := append(s, 12)
fmt.Println("y值", y, "y容量", cap(y))
y3 := append(s, 15, 15, 15)
fmt.Println("y3值", y3, "y3容量", cap(y3))
y4 := append(s, 16)
fmt.Println("y4值", y4, "y4容量", cap(y4))
fmt.Println("s值与容量", s, cap(s), ";y值与容量", y, cap(y), ";y3值与容量", y3, cap(y3), ";y4值与容量", y4, cap(y4))
}
代码执行结果
切片作为函数入参的执行过程及注意事项
- 切片作为入参时,也是值传递方式,也就是 函数内部 会新建一个slice结构体,只是这个结构体每个字段的值和入参slice相同而已,但是 也因为这个特点 导致 执行底层数组的指针也相同,导致对数组每个元素的操作会影响到外部变量; append操作则可能会操作相同的底层数组(除非触发扩容);
示例代码
package main
import (
"fmt"
"reflect"
"unsafe"
)
//
// @Description: 获取切片对应底层数组的指针
// @param s:
// @return unsafe.Pointer:
//
func BottomLayerArrayPoint(s *[]int) unsafe.Pointer {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(s))
return unsafe.Pointer(hdr.Data)
}
func main() {
s := make([]int, 1, 1)
s[0] = 1
//s := []int{1, 1}
fmt.Println("s值为", s, "s地址为", unsafe.Pointer(&s), "s底层数组地址为", BottomLayerArrayPoint(&s),"s底层数组值为",*(*[2]int)(BottomLayerArrayPoint(&s)), "s容量为", cap(s))
f(s)
fmt.Println("s值为", s, "s地址为", unsafe.Pointer(&s), "s底层数组地址为", BottomLayerArrayPoint(&s),"s底层数组值为",*(*[2]int)(BottomLayerArrayPoint(&s)), "s容量为", cap(s))
}
func f(s []int) {
fmt.Println("函数内的s相关 在改变切片元素值前","s值为", s, "s地址为", unsafe.Pointer(&s), "s底层数组地址为", BottomLayerArrayPoint(&s),"s底层数组值为",*(*[2]int)(BottomLayerArrayPoint(&s)), "s容量为", cap(s))
// 改变切片中 元素的值
for i := range s {
s[i] += 1
}
fmt.Println("函数内的s相关 在改变切片元素值后","s值为", s, "s地址为", unsafe.Pointer(&s), "s底层数组地址为", BottomLayerArrayPoint(&s),"s底层数组值为",*(*[2]int)(BottomLayerArrayPoint(&s)), "s容量为", cap(s))
s = append(s, 3)
fmt.Println("函数内的s相关 在append新数据后","s值为", s, "s地址为", unsafe.Pointer(&s), "s底层数组地址为", BottomLayerArrayPoint(&s),"s底层数组值为",*(*[2]int)(BottomLayerArrayPoint(&s)), "s容量为", cap(s))
}
代码执行结果
其他注意事项
遍历slice时修改slice
s := []int{1, 2, 3}
//方法一: 修改失败,v是s元素的拷贝,并不会影响到元素v本身
for _, v := range s {
v = v + 1
}
//方法二: 修改成功
for i, v := range s {
sli[i] = v + 1
}
切片的容量
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
//容量为 包括索引为1到尾部的所有数据个数
b := a[1:2]
fmt.Println(b, cap(b)) // [2] 4
// 容量为 包括索引1到 不包括索引为3的数据个数
c := a[1:2:3]
fmt.Println(c, cap(c)) // [2] 2
}
总结
- 切片存储数据是在底层数组中,而切片和底层数组用 指针关联,因为这个特性会造成 切片间 扩容前的操作相互影响;
- 函数参数类型为切片时,是值引用传递,函数内部新建的切片进行赋值时,由于 切片和底层数组时指针关联,同样会造成 切片的扩容前的操作会影响到 函数外的切片;
- append方法 返回的是新slice,扩容前会影响入参的slice结果,扩容后,不再影响入参的slice结果;
- 只要理解了 切片和底层数组直接的指针关联关系,遇到问题 就看是否扩容 就好解决;