Go - 切片(Slice)的简单介绍与使用

1)简介

Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

2)定义切片

声明一个未指定大小的数组来定义切片:

// 声明一个未指定大小的数组来定义切片
var slice []type

// 使用make函数来创建切片
var slice1 []type = make([]type, len) //len是数组长度,亦是切片的初始长度

// or

slice2 := make([]type, len)

3)初始化切片

// 直接初始化切片s
s := []int {1,2,3}

// 初始化切片s,是数组arr的引用
s := arr[:]

// 以arr中从下标startIndex至endIndex-1间的元素去创建一个新的切片
s := arr[startIndex:endIndex]

// 规则同上,默认至最后一个元素处结束
s := arr[startIndex:]

// 规则同上,默认从第一个元素处开始
s := arr[:endIndex]

// 用切片去初始化切片
s1 := s[startIndex:endIndex]

拓展:len() 与 cap()

len()代表切片的引用长度

cap()代表切片的容量

“容量”的用处:

当用 append扩展长度时,如果新的长度小于容量,不会更换底层数组,否则,go 会新申请一个底层数组,拷贝这边的值过去,把原来的数组丢掉。也就是说,容量的用途是:在数据拷贝和内存申请的消耗与内存占用之间提供一个权衡。

“长度”的用处:

为了在使用时限制切片可用成员的数量,提供边界查询的。所以用 make 申请好空间后,需要注意不要越界(越 len )。

4)空切片

一个切片在未初始化之前默认为 nil,长度为 0。

package main

import "fmt"

func main() {
   var numbers []int

   printSlice(numbers)

   if(numbers == nil){
      fmt.Printf("切片是空的")
   }
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

// 运行结果

len=0 cap=0 slice=[]
切片是空的

5)append() 与 copy()

函数原型

// 往切片中添加元素
func append(slice []Type, elems ...Type) []Type

// 从切片中复制元素,返回复制的元素个数,等于源和目的的最小长度值
func copy(dst, src []Type) int

对于slice进行append操作,遵循以下原则:

1. append 函数对一个切片 slice 进行追加操作,并返回另一个长度为 len(slice) + 追加个数 的切片,原切片不被改动,两个切片所指向的底层数组可能是同一个也可能不是,取决于第二条:

2. slice 是对其底层数组的一段引用,若 append 追加完之后没有突破 slice 的容量,则实际上只是追加的数据改变了其底层数组对应的值,并且 append 函数返回对底层数组新的引用(切片);若 append 追加的数据量突破了 slice 的最大容量(底层数组长度固定,无法增加长度赋予新值),则 Go 会在内存中申请新的数组(数组内的值为追加操作之后的值),并返回对新数组的引用(切片)。

3. 扩容规则:

3.1 在1024字节以内时:

    3.1.1、当同时添加多个元素时,

           3.1.1.1、len[list]+len[params]>=2*cap:

                len(list)+len([params]) 为偶数:cap=len(list)+len([params])

                len(list)+len([params]) 为奇数:cap=len(list)+len([params])+1 

           3.1.1.2、cap<len[list]+len[params]<2*cap:

                cap=2*cap
                 即 cap 始终为偶数。

    3.1.2、当一个一个添加元素时:

          len(list)+1<=cap:   cap=cap

          len(list)+1>cap:     cap=2*cap
                 即 cap 总是呈 2 倍的增加(也是偶数)。

3.2 大于1024字节时,扩容cap的1/4(即newcap=cap+cap*1/4)。


4. 特别说明:本文只是简单性的展示扩容结论,欲深究可以移步这里参考。

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

下面的代码描述了拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)  // 多读两遍上面的“扩容规则”即可理解下边的运行结果

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

// 运行结果

len=0 cap=0   slice=[]
len=1 cap=1   slice=[0]
len=2 cap=2   slice=[0 1]
len=5 cap=6   slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

再来一个栗子进行深入理解:

// 声明并初始化长度为 5 的整型数组 [0 0 0 0 0]
var arr [5]int

// slice1 和 slice2 是对 arr 第 2 个元素到第 4 个元素的引用
slice1 := arr[1:4] // slice1: [0 0 0]
slice2 := arr[1:4] // slice2: [0 0 0]

// 对切片的修改会反映到底层数组
slice1[0] = 1 // slice1:[1 0 0] slice2:[1 0 0] arr:[0 1 0 0 0]
// 对底层数组的修改同样会反映到指向它的切片
arr[2] = 2 // slice1:[1 2 0] slice2:[1 2 0] arr:[0 1 2 0 0]

// 因为对 slice1 的追加没有突破其底层数组的长度,所以返回的切片还是指向原来的底层数组
slice3 := append(slice1, 4) // slice1:[1 2 0] slice2:[1 2 0] slice3:[1 2 0 4] arr:[0 1 2 0 4]
slice3[2] = 3 // slice1:[1 2 3] slice2:[1 2 3] slice3:[1 2 3 4] arr:[0 1 2 3 4]

// 如果对切片的追加突破了底层数组的长度,则会分配一个新的数组,返回指向新数组的切片
slice3 = append(slice3, 5) // slice1:[1 2 3] slice2:[1 2 3] slice3:[1 2 3 4 5] arr:[0 1 2 3 4]
// slice3 的底层数组已经改变,对它的操作不会影响到 slice1 slice2 和 arr
slice3[0] = 6 // slice1:[1 2 3] slice2:[1 2 3] slice3:[6 2 3 4 5] arr:[0 1 2 3 4]

拓展:(1) 合并多个数组

package main
import "fmt"

func main() {
    var arr1 = []int{1,2,3}
    var arr2 = []int{4,5,6}
    var arr3 = []int{7,8,9}
    var s1 = append(append(arr1, arr2...), arr3...)
    fmt.Printf("s1: %v\n", s1)
}

//运行结果
//s1: [1 2 3 4 5 6 7 8 9]

拓展:(2)特别须知:使用 copy 函数要注意对于 copy(dst, src),要初始化 dst 的 size,否则无法复制。

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

// 错误示例:
dst := make([]int, 0)
src := []int{1, 2, 3}
copy(dst, src)
printSlice(src)
printSlice(dst)

//输出结果:
//len=3 cap=3 slice=[1 2 3]
//len=0 cap=0 slice=[]


// 正确示例:
dst := make([]int, 3)  // 令size=3
src := []int{1, 2, 3}
copy(dst, src)
printSlice(src)
printSlice(dst)

//输出结果:
//len=3 cap=3 slice=[1 2 3]
//len=3 cap=3 slice=[1 2 3]

拓展:(3)dst的len初始化为多少,执行copy,就能拷贝多少(超出src范围的copy零值)。当切片有足够大小的时候,append操作是非常快的。但是当给出初始大小后,我们得到的实际上是一个含有这个size数量切片类型的空元素,直接append肯定不可取。此时如果我们既想有好的效率,又想继续使用append函数而不想区分是否有空的元素,此时就要请出make的第三个参数,容量,也就是我们通过传递给make,0的大小和足够大的容量数值就行了。

// 案例1
  var ss=make([]int,10);
  ss=append(ss,111);
  fmt.Println("1after append",ss, len(ss), cap(ss))
    
    
  var ss2=make([]int,0, 10);
  ss2=append(ss2,111);
  fmt.Println("2after append",ss2, len(ss2), cap(ss2))
  
// 输出
1after append [0 0 0 0 0 0 0 0 0 0 111] 11 20
2after append [111] 1 10



// 案例2
  var sa = make ([]int,0);
  for i:=0;i<10;i++{
    sa=append(sa,i)
     
  }
  var da =make([]int,0,10);
  var cc=0;
  cc = copy(da,sa);
  fmt.Printf("copy to da(len=%d)\tda(cap=%d)\t%v\n",len(da),cap(da),da)
  da = make([]int,12, 20)
  cc=copy(da,sa);
  fmt.Printf("copy to da(len=%d)\tda(cap=%d)\tcopied=%d\t%v\n",len(da),cap(da),cc,da)
   da = make([]int,10)
  cc =copy(da,sa);
  fmt.Printf("copy to da(len=%d)\tda(cap=%d)\tcopied=%d\t%v\n",len(da),cap(da),cc,da)

// 输出
copy to da(len=0)	da(cap=10)	[]
copy to da(len=12)	da(cap=20)	copied=10	[0 1 2 3 4 5 6 7 8 9 0 0]
copy to da(len=10)	da(cap=10)	copied=10	[0 1 2 3 4 5 6 7 8 9]

6)Q:基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?

A:对于底层数组容量是 k 的切片 slice[i:j] 来说:

长度: j - i

容量: k - i

// 举个栗子

package main

import "fmt"
func main() {
    numbers:=[]int{0,1,2,3,4,5,6,7,8,9,10}
    printSlice(numbers)
    fmt.Printf("%d\n",numbers[1:3])
    fmt.Printf("%d\n",numbers[2:7])
    fmt.Printf("%d\n",numbers[:3])
    fmt.Printf("%d\n",numbers[4:])
    number1:=make([]int,0,5)
    number2:=numbers[:3]
    printSlice(number1)
    printSlice(number2)
    number3:=numbers[2:5]
    printSlice(number3)  //capacity 为 9 是因为 number3 的 ptr 指向第三个元素, 后面还剩 2,3,4,5,6,7,8,9,10, 所以 cap=9。
    number4:=numbers[3:8]
    printSlice(number4)
}

func printSlice(x []int) {
    fmt.Printf("len=%d  cap=%d   slice=%v\n",len(x),cap(x),x)
}

// 运行结果

len=11  cap=11   slice=[0 1 2 3 4 5 6 7 8 9 10]
[1 2]
[2 3 4 5 6]
[0 1 2]
[4 5 6 7 8 9 10]
len=0  cap=5   slice=[]
len=3  cap=11   slice=[0 1 2]
len=3  cap=9   slice=[2 3 4]
len=5  cap=8   slice=[3 4 5 6 7]

7)slice和array作为函数参数的情况1

package main

import "fmt"

func main(){
  changeSliceTest()
}

func changeSliceTest() {
    arr1 := []int{1, 2, 3}
    arr2 := [3]int{1, 2, 3}
    arr3 := [3]int{1, 2, 3}

    fmt.Println("before change arr1, ", arr1)
    changeSlice(arr1) // slice 按引用传递
    fmt.Println("after change arr1, ", arr1)

    fmt.Println("before change arr2, ", arr2)
    changeArray(arr2) // array 按值传递
    fmt.Println("after change arr2, ", arr2)

    fmt.Println("before change arr3, ", arr3)
    changeArrayByPointer(&arr3) // 可以显式取array的 指针
    fmt.Println("after change arr3, ", arr3)
}

func changeSlice(arr []int) {
    arr[0] = 9999
}

func changeArray(arr [3]int) {
    arr[0] = 6666
}

func changeArrayByPointer(arr *[3]int) {
    arr[0] = 6666
}

// 运行结果

before change arr1,  [1 2 3]
after change arr1,  [9999 2 3]
before change arr2,  [1 2 3]
after change arr2,  [1 2 3]
before change arr3,  [1 2 3]
after change arr3,  [6666 2 3]

    slice作为函数参数的情况2

当把 slice 作为参数进行值传递,本身传递的是值,但就其内容 byte* array,实际传递的是引用,所以可以在函数内部修改;但如果对 slice 做 append,而且导致 slice 进行了扩容,实际扩容的是位于函数内复制的一份切片,对于函数外面的切片本身是不会构成变化的。解决办法是传递切片指针。 

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    slice_test := []int{1, 2, 3, 4, 5}
    fmt.Println(unsafe.Sizeof(slice_test))
    fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))
    slice_value(slice_test)
    fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))
    slice_ptr(&slice_test)
    fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))
    fmt.Println(unsafe.Sizeof(slice_test))
}

func slice_value(slice_test []int) {
    slice_test[1] = 100                // 函数外的slice确实有被修改
    slice_test = append(slice_test, 6) // 函数外的不变
    fmt.Printf("slice_value:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))
}

func slice_ptr(slice_test *[]int) { // 这样才能修改函数外的slice
    *slice_test = append(*slice_test, 7)
    fmt.Printf("slice_ptr:%#v,%#v,%#v\n", *slice_test, len(*slice_test), cap(*slice_test))
}

// 运行结果:

24
main:[]int{1, 2, 3, 4, 5},5,5
slice_value:[]int{1, 100, 3, 4, 5, 6},6,10
main:[]int{1, 100, 3, 4, 5},5,5
slice_ptr:[]int{1, 100, 3, 4, 5, 7},6,10
main:[]int{1, 100, 3, 4, 5, 7},6,10
24

8)参考文献

Go 语言切片(Slice) | 菜鸟教程

go cap和len的区别_nzz_1214-CSDN博客_cap go

Go语言中append()函数的扩容实验_流年入笔-CSDN博客_go中append

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值