您的当前位置:首页正文

Go Slice详解

2024-11-29 来源:个人技术集锦

slice的存储结构

slice代表变长的序列,它的底层是数组。一个切片由3部分组成:指针、长度和容量。指针指向底层数组,长度代表slice当前的长度,容量代表底层数组的长度。
换句话说,slice自身维护了一个指针属性,指向它底层数组的某些元素的集合。

type slice struct {
    array unsafe.Pointer // 指向底层数组
    len int // 长度
    cap int // 容量
}

创建切片

一般有如下方法创建切片。

slice := make([]T, 5)

上述代码创建了一个整型切片,其长度为5。若不传入容量的大小,则容量和长度相同。

slice := make([]T, 3, 5)

上述代码创建了一个整型切片,其长度为3,容量为5。

  1. 通过字面量创建切片

slice := []int{1, 2, 3, 4}

上述代码创建了长度和容量均为4的整型切片。

  1. 通过切片创建切片

slice[i:j:k]

i代表起始位置,切片的长度为(j-i),切片的容量为(k-i)。如果没有指定k,则表示切到底层数组的尾部。此外还有几种简化形式:

slice[i:] // 从i切到尾部
slice[:j] // 从头部切到j,不包括j
slice[:] // 从头切到尾

nil slice和空slice

声明一个slice,但是不初始化,这个slice就是nil slice。nil slice表示它的指针为nil,也就是这个slice不会指向底层数组,因此它的长度和容量都为0。

var slice []int

创建一个长度为0的slice,就是空slice。空slice的长度和容量也为0,但是它会指向一个底层数组,只不过底层数组是长度为0的空数组。

slice := make([]int,0)

copy()函数

可以将一个slice拷贝到另一个slice中。copy()函数表示将src拷贝到dst。若src比dst长,则截断;若src比dst短,则只拷贝src的部分。返回值是拷贝成功的元素数量。

func copy(dst, src []Type) int

示例:

s1 := []int{11, 22, 33}
s2 := make([]int, 5)
s3 := make([]int,2)

num := copy(s2, s1)
copy(s3,s1)

// [11 22 33] 0xc0000160a8
fmt.Printf("the value of s1 is %v, the value of ptr of s1 is %p\n", s1, s1) 
// [11,22,33,0,0] 0xc00001c120
fmt.Printf("the value of s2 is %v, the value of ptr of s2 is %p\n", s2, s2)
// [11,22] 0xc00001a2d0 
fmt.Printf("the value of s3 is %v, the value of ptr os s3 is %p\n", s3, s3) 

s1拷贝到s2时,因s1的长度小于s2的长度,只拷贝s1的部分,则s2为[11,22,33,0,0];s1拷贝到s3时,因s3的长度小于s1的长度,所以截断s1,只拷贝前两个元素,则s3为[11, 22]。
copy()操作只是拷贝内容,各切片的底层数组仍然是独立的。

append()函数

使用append()函数可以追加元素。
在append时,如果切片的容量已经不能容纳将要追加的数据,就会创建一个新的扩容后的底层数组,将之前的数据拷贝过去后,再执行扩容操作;如果切片的容量足以容纳,那么就会在原数组执行扩容操作。
目前扩展底层数组的逻辑为:按照当前底层数组长度的2倍进行扩容;如果底层数组的长度超过1000,将按照125%扩容。
下述代码分别测试了在不会扩容和会扩容的前提下,执行append操作的结果。

func main() {
  var slice = []int{1, 2, 3, 4, 5} // len = 5; capacity = 5
  var newSlice = slice[1:3]        // len = 2; capacity = 4(已经使用了两个位置,还有两个位置可以append)

  fmt.Printf("%p\n", slice)    // 0xc00001c120
  fmt.Printf("%p\n", newSlice) // 0xc00001c128; newSlice的地址指向的是slice[1]的地址,因此底层使用的是同一个数组

  fmt.Printf("%v\n", slice)    // [1 2 3 4 5]
  fmt.Printf("%v\n", newSlice) // [2 3]

  newSlice[1] = 6              // 更改后slice、newSlice都改变了
  fmt.Printf("%v\n", slice)    // [1 2 6 4 5]
  fmt.Printf("%v\n", newSlice) // [2 6]

  newSlice = append(newSlice, 7, 8) // append操作之后,array的len和capacity不变, newArray的len变为4,capacity仍然为4
  fmt.Printf("%v\n", slice)         //[1 2 6 7 8]; newSlice改变了底层数组的内容,所以slice的内容也变了
  fmt.Printf("%v\n", newSlice)      //[2 6 7 8]

  newSlice = append(newSlice, 9, 10) // newSlice的len已经等于cap,再次append会创建一个新的底层数组(已扩容),并将array指向的底层数组拷贝过去,并追加新值。
  fmt.Printf("%p\n", slice)          // 0xc00001c120; slice指向的底层数组未改变
  fmt.Printf("%p\n", newSlice)       // 0xc00009e000; newSlice指向的底层数组有改变
  fmt.Printf("%v\n", slice)          // [1 2 6 7 8]
  fmt.Printf("%v\n", newSlice)       // [2 6 7 8 9 10]
}

slice传参

在Go语言中,函数的参数都是按值传递的,因此在调用函数时,会将参数的副本传递给函数。在传递slice时,虽然传递的是副本,但是副本同样指向了源slice的底层数组,所以在函数内部修改slice,有可能会影响到底层数组,进而影响到其他slice。

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(slice)
  // [3 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) {
  slice[0] = 3
  // [3 2] 0xc00000e090 0xc00001a2d0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(slice)
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) {
  slice = append(slice, 3)
  // [1 2 3] 0xc00000e090 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

上述代码在change()内部为slice追加元素3,由于slice的长度和容量均为2,append()操作会导致slice的副本指向一个新的底层数组,因此slice的副本和slice指向的底层数组不再为同一个。
在调用change()后,将slice下标为0的值修改为4,输出slice的值为[4,2]而非[1,2,3],再次说明了change()函数内部的slice和main()函数的slice已经不再指向同一个底层数组。
如果希望获取到函数调用后的slice(),有如下两种方法:

  1. 函数调用返回slice

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice = change(slice)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) []int {
  slice = append(slice, 3)
  // [1 2 3] 0xc00000e090 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return slice
}
  1. 参数传入指针

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(&slice)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice *[]int) {
  *slice = append(*slice, 3)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", *slice, slice, *slice)
}

切片的地址和切片的指针指向的地址

func main() {
  slice := make([]int, 2, 5)
  slice1 := slice
  slice[1] = 3
  fmt.Printf("slice is %v, slice1 is %v\n", slice, slice1)                                 // [0 3] [0 3]
  fmt.Printf("addr of slice is %p, addr of slice1 is %p\n", &slice, &slice1)               // 0xc00000e048 0xc00000e060
  fmt.Printf("value of ptr of slice is %p, value of ptr of slice1 is %p\n", slice, slice1) // 0xc00001c120 0xc00001c120
}
显示全文