切片(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)
}

tif切片存MongoDB存的是什么形式 切片存储_数组

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))
}

代码执行结果

tif切片存MongoDB存的是什么形式 切片存储_数据_02

切片作为函数入参的执行过程及注意事项

  • 切片作为入参时,也是值传递方式,也就是 函数内部 会新建一个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))
}

代码执行结果

tif切片存MongoDB存的是什么形式 切片存储_数组_03

其他注意事项

遍历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结果;
  • 只要理解了 切片和底层数组直接的指针关联关系,遇到问题 就看是否扩容 就好解决;