概述

切片 slice 序列的长度可以变化,可以往切片里新增,或删除元素。

切片实际上由两部分组成:结构体 + 实际存储数据的数组

切片结构体信息

结构体里有三个属性,用于存储一些必要信息:

  1. slice[0] 元素的指针地址,也就是切片第 1 个元素所在的底层数组位置
  2. 切片的长度,表示已经有值的数组
  3. 容量,表示预先分配好的数组内存空间

底层数组

实际存储数据的数组,通过指针和切片结构体联系在一起。

声明

var 关键字

切片的声明也挺简单,如下

// 声明string类型的切片
var cityList []string

但是需要注意的是,因为切片是引用类型,声明时内存中还没有引用数组,所以此时的零值默认是 nil

var cityList []string
if cityList == nil {
    fmt.Println("nil")// 将会输出
}

还有一种声明方式,如下所示,它们是等价的,第二种方式

func main() {
    // 方式1
    var a []int

    // 方式2
    b := []int(nil) // 等效方式1
    c := append([]int(nil), 1) // 可以不用var关键字声明后再使用
    fmt.Println(a == nil, b == nil, c)
}

切片表达式

如下所示,声明一个 months 月份数组

var months = [...]string{  
   1:  "一月",  
   2:  "二月",  
   3:  "三月",  
   4:  "四月",  
   5:  "五月",  
   6:  "六月",  
   7:  "七月",  
   8:  "八月",  
   9:  "九月",  
   10: "十月",  
   11: "十一月",  
   12: "十二月",  
}

在 months 数组中,一月份是 months[1],十二月份是 months[12]。通常,数组的第一个元素从索引 0 开始,但是月份一般是从 1 开始的,因此我们声明数组时直接跳过第 0 个元素,第 0 个元素会被自动初始化为空字符串。

slice 的切片操作 s[i:j]

  1. ij 都数组的下标,遵循左闭右开的原则,
  2. 新的 slice 将只有 j-i 个元素。
  3. 如果 i 省略,就是 0,表示从头开始切
  4. 如果 j 省略,就是数组长度,表示切到数组末尾。

因此,举例说明:

  1. months[1:13] 切片操作将引用全部有效的月份,和 months[1:] 操作等价;
  2. months[:] 切片操作则是引用整个数组。

l5wa30pe.png

让我们分别定义表示第二季度和北方夏天月份的 slice,它们有重叠部分:

Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     //[四月 五月 六月]
fmt.Println(summer) // [六月 七月 八月]

如果切片操作超出 cap 上限将导致一个 panic 异常

aa := months[:14]
fmt.Println(aa) // panic invalid argument: index 14 out of bounds [0:14]

可以对切片再进行切片操作,只要不要超过该切片的 cap 就可以

endlessSummer := summer[:5] // extend a slice (within capacity) 
fmt.Println(endlessSummer) // "[June July August September October]"

可以指定切片的最大上限,表达式是 a[low : high : max],此时得到的结果切片的容量为 max-low

a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
// 5-1 = 4,所以容量是4
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))

初始化

定义的时候使用字面量的形式初始化

var a = []int{1,2,3}

使用 make 函数构造切片的时候,也会自动初始化

a := make([]int, 2, 10)

获取切片的元素值

基本上和数组访问元素是一样的,都是通过下标进行访问,但是需要注意的是,切片如果没有初始化的话,则不允许访问

var a []int // 没有初始化,在内存中没有位置
a[0] = 1    // 编译没问题,运行的时候会报错

b := []int{1, 2} // 初始化
b[2] = 1         // 可以正常设置

切片之间进行比较

切片之间是不能比较,不能使用 == 来判断两个切片是否含有全部相等元素,唯一合法的比较操作是和 nil 比较。

  1. 一个 nil 值的切片并没有底层数组(也就是没有初始化)
  2. 一个 nil 值的切片的长度和容量都是 0
  3. 不能说一个长度和容量都是 0 的切片一定是 nil
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

始终使用 len(s) == 0 来判断一个切片是否为空,而不应该使用 s == nil 来判断,或者需要自己遍历切片,一个个元素去比较。

切片赋值拷贝

切片是引用类型,赋值后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容

s1 := make([]int, 3) //[0 0 0]
s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100          // 修改切片元素值
fmt.Println(s1)      //[100 0 0]
fmt.Println(s2)      //[100 0 0]

copy 的时候,可以copy部分数据,以下是gin框架里的源码

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    assert1(finalSize < int(abortIndex), "too many handlers")
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    // copy 到后半部分
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}

因为切片其实就是一个结构体,里面有指针指向底层数组,所以在函数里修改底层数组,外层也会改变

func setHobby(hobby []string) bool {
    hobby[1] = "bb"
    return true
}
func main() {
    hobby := []string{"a", "b", "c"}
    setHobby(hobby)
    fmt.Printf("%#v", hobby) // []string{"a", "bb", "c"}
}

但是如果是 append 的操作,或 delete 操作的话,需要传递指针,不然不会改变,因为此时的切片结构体的 len 已经变成了 4 了,已经不是原来 aa 这个切片结构体了,如下图,append 操作后,arr 和 brr 不是同一个了,它们的长度不一样

lqv2izv3.png

func AppendEle(s []int) {
    s = append(s, 9)
}

func RemoveEle(s []int) {
    s = (s)[0:1]
}

func main() {
    aa := make([]int, 3)
    AppendEle(aa)
    fmt.Println(aa) // [0 0 0]
    RemoveEle(aa)
    fmt.Println(aa) // [0 0 0]
}

需要改成切片结构体改成指针类型,在函数里修改切片结构体变量内容,也就长度会被修改

func AppendEle(s *[]int) {
    *s = append(*s, 9)
}

func RemoveEle(s *[]int) {
    *s = (*s)[0:1]
}

func main() {
    aa := make([]int, 3)
    AppendEle(&aa)
    fmt.Println(aa) // [0 0 0]
    RemoveEle(&aa)
    fmt.Println(aa) // [0]
}

如果想要拷贝一份独立的数据,需要用 copy 函数

// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5],不会改变a切片

往切片里增加元素

使用 append() 函数,可以为切片动态添加元素:

  1. 返回值必须赋值给一个接受变量
  2. 底层数组容量不足时,append 函数会自动按照一定的策略进行"扩容"
  3. 尽量避免扩容,减少性能消耗
  4. 不能通过索引下标的方式进行扩容
sli := make([]int, 2, 3)
sli[2] = 4 // 运行的时候将报错,因为长度只有2,索引不能超过1
fmt.Println(sli)

通过 var 声明的零值切片可以在 append() 函数直接使用,无需初始化

var s []int // 无需初始化
s = append(s, 1)
fmt.Println(s)

对原来数进行切片的扩容,好像不会影响原来数组本身,因为会重新 make 一个新的切片,并且容量是它的两倍

var array = [5]int{1, 2, 3, 4, 5}
s := array[3:]
s = append(s, 6)
s[0] = 100
fmt.Println(array)  // [1 2 3 4 5],原来的数组不会受到影响
fmt.Println(s)      //[4 5 6]
fmt.Println(len(s)) //3
fmt.Println(cap(s)) //4

对数组进行切片后,修改切片的话,会影响原来的数组

var array = [5]int{1, 2, 3, 4, 5}
s := array[3:]
s[0] = 5555
fmt.Println(array) // [1 2 3 5555 5]

还可以可以一次性增加多个元素

var s []int
s = append(s, 1, 2, 3, 4, 5, 6)
fmt.Println(s)

添加另一个切片中的元素,注意要加 ...

var s []int
s1 := []int{1, 2, 3, 4, 5} // 需要初始化
s = append(s, s1...)       // 后面需要添加...
fmt.Println(s)

删除元素

Go 本身没有删除切片元素的专用方法,可以使用切片本身的特性来删除元素

// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]

排序

slices.SortFunc(remoteDataList, func(a, b remoteDataType) bool {
   if extra.IsReverse {
        return a.CommitTime < b.CommitTime
  } else {
        return b.CommitTime < a.CommitTime
  }
})

面试题

下面代码的输出是什么

func main() {
    var arr [3]int
    brr := arr[1:2]
    brr = append(brr, 9)
    brr = append(brr, 9)
    fmt.Println(arr, brr, cap(brr)) // [0 0 9] [0 9 9] 4
}

这个切片可以理解为是个结构体,里面有三个属性:

  1. 第一个是底层数组的首地址
  2. 第二个是长度,目前切片里的元素,也就是黄色的那个
  3. 第三是容量,容量是2,切片截取是从1开始截取,1之后的都是容量

lu3j0g0f.png

然后 append(brr,9) 的操作,会改掉底层数组绿色的值,因为这个还是切片的容量

lu3j2iku.png

然后在来个 append(brr,9) 的操作,这个时候,底层数组的容量不够了,需要新申请一份数组内存,容量是原来的两倍,原来是2,那现在新的就是4,然后把原来的0和9拷贝到新的数组这边过来,接着再这新的数组这边append,所以如下所示

lu3j9qm6.png

最后修改:2024 年 03 月 23 日
如果觉得我的文章对你有用,请随意赞赏