切片Slice

* 本页面主要介绍Go语言引用数据类型切片Slice的相关内容。

正如我们在 数组 了解到的,数组在诸多方面并不是很灵活,应用的非常之少。那么今天要学习的切片,相对的则是随处可见的,Go语言中,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷

一、基本概念

切片(Slice)代表变长的序列,序列中每个元素都有相同的类型。slice类型一般写作[]T,其中T代表slice中元素的类型(和数组的区别就是[]没有了数值,即容量)。举例如下:

  var sli []int
  var months []string

我们可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组(匿名的数组变量)。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用

一个slice由三个部分构成:指针、长度和容量

  • 指针:指向第一个slice元素对应的底层数组元素的地址
  • 长度:对应slice中元素的数目,长度不能超过容量,可以用内置函数len得到
  • 容量:一般是从slice的开始位置到底层数据的结尾位置,可以使用内置函数cap得到

特别说明:如果 slice == nil,那么 len、cap 结果都等于 0。

还有一点,若果是空切片,它的len和cap也是0,但是和nil切片指向底层数组的指针不同:nil切片的指针是nil,而空切片对应的指针是个地址。

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠

二、切片的创建和使用
  //通过make创建
  //创建一个长度为5,容量为10的切片
  //如果不指定容量,那么容量和长度相等
  slice := make([]int, 5, 10)
  //通过字面量创建
  myStr := []string{"Jack", "Mark", "Nick"}
  myNum := []int{10, 20, 30, 40}
  //只初始化某个索引的值,其他索引默认零值
  myNums := []int{4:1}
  //获取值
  fmt.Println(myNum[2])
  //修改值
  myNum[2] = 5
  //越界访问会导致运行时异常 panic: out of range
  fmt.Println(myNum[10]) 

可以注意到,通过字面量声明切片时跟声明数组非常相似,唯一的区别就是声明切片的时候[]运算符里没有指定明确的数字(容量)。

slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。除了[]byte类型可以使用bytes.Equal函数来判等之外,其他的任何切片都需要手动展开每一项进行比较。

slice唯一合法的比较操作是和nil比较。

  func equal(x, y []string) bool {
      if len(x) != len(y) {
          return false
      }
      for i := range x {
          if x[i] != y[i] {
              return false
          }
      }
      return true
  }

之所以不支持切片比较,主要基于以下两个原因:

  • slice的元素是间接引用的,一个slice甚至可以包含自身,比较起来非常复杂
  • 一个固定的slice值(slice本身)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。
三、切片的内存结构

我们用下面这个例子来说明下切片在内存中的结构是怎样的。

  months := [...]string{1: "January", /* ... */, 12: "December"}
  Q2 := months[4:7]
  summer := months[6:9]
  fmt.Println(Q2)     // ["April" "May" "June"]
  fmt.Println(summer) // ["June" "July" "August"]

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。所以Q2和summer有一个月的重叠部分,结构见下图:

切片的内存结构
切片的内存结构

有一个有意思的写法,你可能没有见到过,其实切片的这个操作还可以有“第三个参数”

  
  s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  s1 := s[2:5]    // [2 3 4]
  s2 := s1[2:6:7] // [4 5 6 7]

  fmt.Println(s1, s2, len(s2), cap(s2))
  //[2 3 4] [4 5 6 7] 4 5

在这个写法中,“第三个参数”标识的是容量,即容量结束的位置。比如上面示例中,这个7,代表容量从2 -> 7,所以,这时候就是容量从2~7是它的开始和结束位置。另外需要注意,SLICEX[A:B:C] 这种写法时,B和C不要超过SLICEX的容量即可,与length无关。

四、append()函数

内置的append函数用于向slice追加元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时。

  slice := []int{1, 2, 3, 4, 5}
 newSlice := slice[1:3]
 newSlice=append(newSlice,10)
  //同时追加多个
  newSlice=append(newSlice,20,30,40)
  //追加一个切片到另一个切片中
  newSlice2 := slice[2:4]
  newSlice=append(newSlice,newSlice2…)

目前的扩容策略是:容量小于1024个时,总是成倍的增长,一旦容量超过1024个,增长因子设为1.25,也就是说每次会增加25%的容量。如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。

五、copy()函数

由于切片是引用类型,所以通过普通的赋值操作,会指向同一个内存地址,对“新变量”的修改,也会影响到“老变量”的值,那么怎么办呢?这时就可以使用内置的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]
六、删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素:要从切片a中删除索引为index的元素,操作方法是 a = append(a[:index], a[index+1:]...)

  //从切片中删除元素
  a := []int{30, 31, 32, 33, 34, 35, 36, 37}
  // 要删除索引为2的元素
  a = append(a[:2], a[3:]...)
  fmt.Println(a) //[30 31 33 34 35 36 37]
七、切片排序

如果我们相对一个切片,按照其中某个字段,进行排序,要怎么实现呢?可以参考下面的示例完成排序:

  package main
 
  import (
    "fmt"
    "sort")
   
   
  type Person struct {
    Age  int
    Name string
  }
   
  func main() {

    var personArr []*Person
   
    p1 := &Person{Age: 1, Name: "小明"}
    p2 := &Person{Age: 5, Name: "小红"}
    p3 := &Person{Age: 4, Name: "小李"}
    p4 := &Person{Age: 2, Name: "小杰"}
   
    personArr = append(personArr, p1)
    personArr = append(personArr, p2)
    personArr = append(personArr, p3)
    personArr = append(personArr, p4)
   
    //按照最大值排序
    sort.Slice(personArr,func (i int,j int) bool {
      return personArr[i].Age < personArr[j].Age
    })
    for _,v := range personArr {
      fmt.Println(v)
    } 
  }

如果是一个简单的整形切片,可以如下面的例子完成排序:

  package main
 
  import "fmt"
   
  /*对切片排序*/
  func SortSlice(slice []int) {
      for i := 0; i < len(slice)-1; i++ {
          //遍历i位以后的所有元素,如果比i位元素小,就和i位元素互换位置
          for j:=i+1;j < len(slice) ;j++  {
              if slice[j] < slice[i]{
                  slice[i],slice[j] = slice[j],slice[i]
              }
          }
      }
  }
   
  func main() {
      slice := []int{6, 8, 3, 5, 9, 7, 1, 4, 2, 0}
      SortSlice(slice)
      fmt.Println(slice)
  }

好,切片相关的知识点就总结到这里,接下来进入 字典 的学习吧!


* 本页内容更新时间线:

  • 2021年07月29日 11:28:22 : 增加切片排序部分
  • 2021年04月25日 10:50:50 : 更新切片“第三参数”说明
  • 2020年11月17日 20:03:34 : 创建文档

* 本页内容参考以下数据源:

  • 《Go程序设计语言》
  • 《Go语言核心36讲》 07 数组和切片
  • https://blog.csdn.net/li123128/article/details/84620169
  • https://studygolang.com/articles/30865
  • https://studygolang.com/articles/25059
  • https://blog.csdn.net/phpduang/article/details/114004891
  • https://blog.csdn.net/wjl18270365476/article/details/118933107

凯冰科技 · 代码改变世界,技术改变生活
Next Page→