slice

切片是不定长的特定元素类型的序列,可以理解为动态数组

Go语言中,数组在传递的时候,传递的是原数组的拷贝,对大数组来说,内存代价会非常大,影响性能。

传递数组指针可以解决这个问题,但是数组指针也有一个弊端:原数组的指针指向改变了,那函数里面的指针指向也会跟着改变,某些情况下,可能会产生意想不到的bug。slice的出现,便是为了解决这个问题。

一、特点

  • 长度不固定
  • 切片是引用类型,一般来说是浅拷贝
  • 切片本身不能存储任何数据,都是底层数组存储数据,修改切片的时候修改的是底层数组中的数据,切片一旦扩容,会指向一个新的底层数组,内存地址也就随之改变。
  • 底层实现是一个结构体,包括长度、容量和一个指向实际数组的unsafe.Pointer指针
1
2
3
4
5
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int // 长度 
    cap   int // 容量
}

注意:

底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

二、扩容规则

  • 如果扩容需求大于当前容量的两倍,扩容后的容量为所需的最小容量
  • 当前切片长度<1024,扩容当前容量为2倍,
  • 当前切片长度>1024,每次扩容当前容量的1.25倍,循环扩容直至容量满足需求
    切片扩容之后,指向匿名数组的指针地址会发生变化。

1.18 引入了新的扩容规则,首先 1024 的边界不复存在,取而代之的常量是 256 。超出256的情况,也不是直接扩容25%,而是设计了一个平滑过渡的计算方法,随着容量增大,扩容比例逐渐从100%平滑降低到25%,从 2 倍平滑过渡到 1.25 倍。

为什么要这样设计?

  避免追加过程中频繁扩容,减少内存分配和数据复制开销,有助于性能提升。

计算出了新容量之后,还没有完,出于内存的高效利用考虑,还要进行内存对齐。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。

三、声明与赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//  只声明
var a = []string
// 声明,使用字面量初始化
var b = []string{"a", "b", "c"}

// 从已有数组或切片生成
var c = b[1:]

// 使用make生成 可以指定长度,容量
var d = make([]string, 3)
var d = make([]string, 3, 8)

// new方式创建的是指针
var e *[]int = new([]int)

注意:

  1. 只声明的切片 等于nil,长度与容量都为0
  2. make([]string, 0, 0)赋值的切片,长度与容量都为0,但不是nil
  3. make会用零值0初始化所有元素, cap可心省略(默认等于长度)

四、切片slice和数组array的关系

  • 切片slice的底层是对数组array的引用;

  • 切片可以引用数组的部分元素或者全部元素;

  • 切片slice的指针指向的是切片的第一个元素的内存地址,也就是该元素对应的数组的元素的内存地址。

五、切片操作

  1. len,cap

可以查看切片的长度与容量

  1. append

s = append(s, “x”)

s作为参数传给函数append是值传递,是结构体的拷贝,底层数组的指针一样

底层数组加了一个元素,s拷贝的长度也会跟着变,但s的长度没变啊,所以s要重新赋值

另外如果扩容了s拷贝的底层数组指针会变, 长度,容量也会变,但原来的s什么都不会变,所以s也要重新赋值

值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。

函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础,

最外面的原始切片是否改变,取决于函数内的操作和切片本身容量,是否修改了底层数组。

  • 如果要修改切片的值,那么一定对底层数组做了修改,为影响到函数外的切片
  • 如果是append操作,则要看切片是否扩容
    • 切片没有进行扩容,那么会直接添加或修改切片指向底层数组中后一位的值,故底层数组会受到改变,函数外切片改变;
    • 而如果进行扩容,则会导致切片指向一个新的底层数组,一切修改都对函数外的原切片无影响
  1. 访问

切片的引用方式是[ start, end )半闭区间的模式,即索引start可以引用到,而end是不能引用到的

start可以没有,默认为0;end可以没有默认cap

  1. 删除

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    s := []int{0, 1, 2, 3, 4, 5}
    // 删除go切片首尾元素的方法
    s = s[1:]             //利用切片引用并重新赋值的方法,删除掉首尾元素,如果想删除两个,可以用s=s[2:]
    fmt.Println(s)        //[1,2,3,4,5]
    s = s[0:(len(s) - 1)] //删除末尾的元素
    fmt.Println(s)        //[1,2,3,4]
    
    // 接下来,我们利用append()方法来删除切片中间位置的元素
    s = append(s[:1], s[2:]...)
    fmt.Println(s) //[1,3,4]
    
  2. 复制

    切片共用底层数组,修改的话有可能影响原来的切片,如果我们不想这样,可以使用copy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    s0 := []int{1, 0, 3, 2, 6, 5}
    s1 := make([]int, len(s0))
    s2 := s0
    copy(s1, s0)
    fmt.Printf("s0的内存地址为%p,s1的内存地址为%p,s2的内存地址为%p \n", &s0, &s1, &s2)
    s1[0] = 100
    fmt.Printf("s1元素修改后,s1的值为%v, s0的值为%v \n", s1, s0)
    s0[0] = 99
    fmt.Printf("s0元素修改后,s0的值为%v,s2的值为%v,s1的值为%v \n", s0, s2, s1)
    

    s0,s2相互影响,而s1则跟s0,s2没有关系