目录
  • 数组
  • 切片
    • 切片的创建
      • 直接声明
      • new方式初始化
      • 字面量
      • make方式
      • 截取方式
      • s[:]
      • s[i:]
      • s[:j]
      • s[i:j]
      • s[i:j:x]
      • 看个例子
    • 切片的扩容
      • 内存对齐
    • 空切片和nil切片
      • 数组是值传递,切片是引用传递?
        • 数组和slice能不能比较
          • 只有长度相同,类型也相同的数组才能比较
          • slice只能和nil做比较,其余的都不能比较

      数组

      go开发者在日常的工作中slice算是用的比较多的了,在介绍slice之前,我们先了解下数组,数组相信大家都不陌生,数组的数据结构比较简单,它在内存中是连续的。以一个存了10个数字的数组为例来说:

      a:=[10]int{0,1,2,3,4,5,6,7,8,9}
      

      它在内存中大概是这样的:

      golang之数组切片的具体用法

      得益于连续性,所以数组的特点就是:

      • 大小固定
      • 访问快,复杂度为O(1);
      • 插入和删除元素因为要移动元素,所以相比查询会慢。 当我们要访问一个越界的元素的元素时,go甚至编辑都不通过:
      a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
      fmt.Println(a[10])
      // invalid array index 10 (out of bounds for 10-element array)
      

      切片

      相比数组,go的slice(切片)要相对灵活些,比较大的不同点就是slice的长度可以不固定,创建的时候不用指明长度,在go中slice是一种设计过的数据结构:

      type slice struct {
         array unsafe.Pointer //指针
         len   int //长度
         cap   int //容量
      }
      

      slice的底层其实还是数组,通过指针指向它底层的数组,len是slice的长度,cap是slice的容量,slice添加元素时,且cap容量不足时,会根据策略扩容。

      golang之数组切片的具体用法

      切片的创建

      直接声明

      var s []int
      

      通过直接声明的slice,它是个nil slice,它的长度和容量都是0,且不指向任何底层数组,nil切片和空切片是不一样的,接下来会介绍。

      new方式初始化

      s:=*new([]int) 
      

      new的方式和直接声明的方式区别不大,最终产出的都是一个nil的slice。

      字面量

      s1 := []int{0, 1, 2}
      s2 := []int{0, 1, 2, 4: 4}
      s3 := []int{0, 1, 2, 4: 4, 5, 6, 9: 9}
      fmt.Println(s1, len(s1), cap(s1)) //[0 1 2] 3 3
      fmt.Println(s2, len(s2), cap(s2)) //[0 1 2 0 4] 5 5
      fmt.Println(s3, len(s3), cap(s3)) //[0 1 2 0 4 5 6 0 0 9] 10 10
      

      字面量创建的slice,默认长度和容量是相等的,需要注意的是如果我们单独指明了某个索引的值,那么在这个索引值前面的元素如果未声明的话,就会是slice的类型的默认值。

      make方式

      s := make([]int, 5, 6)
      fmt.Println(s, len(s), cap(s)) //[0 0 0 0 0] 5 6
      

      通过make可以指定slice的长度和容量。

      截取方式

      切片可以从数组或者其他切片中截取获得,这时新的切片会和老的数组或切片共享一个底层数组,不管谁修改了数据,都会影响到底层的数组,但是如果新的切片发生了扩容,那么底层的数组就不是同一个。

      s[:]

      a := []int{0, 1, 2, 3, 4}
      b := a[:]
      fmt.Println(b, len(b), cap(b)) //[0 1 2 3 4] 5 5
      

      通过: 获取 [0,len(a)-1]的切片,等同于整个切片的引用。

      s[i:]

      a := []int{0, 1, 2, 3, 4}
      b := a[1:]
      fmt.Println(b, len(b), cap(b)) //[1 2 3 4] 4 4
      

      通过指定切片的开始位置来获取切片,它是左闭的包含左边的元素,此时它的容量cap(b)=cap(a)-i。这里要注意界限问题,a[5:]的话,相当于走到数组的尾巴处,什么元素也没了,此时就是个空切片,但是如果你用a[6:]的话,那么就会报错,超出了数组的界限。

      a := []int{0, 1, 2, 3, 4}
      b := a[5:] //[]
      c := a[6:] //runtime error: slice bounds out of range [6:5]
      

      c虽然报错了,但是它只是运行时报错,编译还是能通过的

      s[:j]

      a := []int{0, 1, 2, 3, 4}
      b := a[:4]
      fmt.Println(b, len(b), cap(b)) //[0 1 2 3] 4 5
      

      获取[0-j)的数据,注意右边是开区间,不包含j,同时它的cap和j没关系,始终是cap(b) = cap(a),同样注意不要越界。

      s[i:j]

      a := []int{0, 1, 2, 3, 4}
      b := a[2:4]
      fmt.Println(b, len(b), cap(b)) //[2 3] 2 3
      

      获取[i-j)的数据,注意右边是开区间,不包含j,它的cap(b) = cap(a)-i

      s[i:j:x]

      a := []int{0, 1, 2, 3, 4}
      b := a[1:2:3]
      fmt.Println(b, len(b), cap(b)) //[1] 1 2
      

      通过上面的例子,我们可以发现切片b的cap其实和j没什么关系,和i存在关联,不管j是什么,始终是cap(b)=cap(a)-ix的出现可以修改b的容量,当我们设置x后,cap(b) = x-i而不再是cap(a)-i了。

      看个例子

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

      s1是对s0的切片,所以它们大概是这样:

      golang之数组切片的具体用法

      s2 := s1[1:3:4]
      

      这时指定个s2,s2是对s1的切片,并且s2的len=2,cap=3,所以大概长这样:

      golang之数组切片的具体用法

      s1[1] = 40
      fmt.Println(s0, s1, s2)// [0 1 2 3 40 5 6 7 8 9] [3 40 5] [40 5]
      

      这时把s1[1]修改成40,因为没有涉及到扩容,s0、s1、s2重叠部分都指向同一个底层数组,所以最终发现s0、s2对应的位置都变成了40。

      golang之数组切片的具体用法

      s2 = append(s2, 10)
      fmt.Println(s2, len(s2), cap(s2)) //[40 5 10] 3 3
      

      再向s2中添加一个元素,因为s2还有一个空间,所以不用发生扩容。

      golang之数组切片的具体用法

      s2 = append(s2, 11)
      fmt.Println(s2, len(s2), cap(s2)) //[40 5 10 11] 4 6
      

      继续向s2中添加一个元素,此时s2已经没有空间了,所以会触发扩容,扩容后指向一个新的底层数据,和原来的底层数组解耦了。

      golang之数组切片的具体用法

      此时无论怎么修改s2都不会影响到s1和s2。

      切片的扩容

      slice的扩容主要通过growslice函数上来处理的:

      func growslice(et *_type, old slice, cap int) slice {
          ....
          newcap := old.cap
          doublecap := newcap + newcap
          if cap > doublecap {
                  newcap = cap
          } else {
              if old.len < 1024 {
                    newcap = doublecap
              } else {
                  // Check 0 < newcap to detect overflow
                  // and prevent an infinite loop.
                  for 0 < newcap && newcap < cap {
                        newcap += newcap / 4
                  }
                  // Set newcap to the requested cap when
                  // the newcap calculation overflowed.
                  if newcap <= 0 {
                       newcap = cap
                  }
              }
          }
          ....
          return slice{p, old.len, newcap}
      }
      

      入参说明下:

      • et是slice的类型。
      • old是老的slice。
      • cap是扩容后的最低容量,比如原来是4,append加了一个,那么cap就是5。 所以上面的代码解释为:
      • 如果扩容后的最低容量大于老的slice的容量的2倍,那么新的容量等于扩容后的最低容量。
      • 如果老的slice的长度小于1024,那么新的容量就是老的slice的容量的2倍
      • 如果老的slice的长度大于等于1024,那么新的容量就等于的容量不停的1.25倍,直至大于扩容后的最低容量。 这里需要说明下关于slice的扩容网上很多文章都说小于1024翻倍扩容,大于1024每次1.25倍扩容,其实就是基于这段代码,但其实这不全对,我们来看个例子:
      a := []int{1, 2}
      fmt.Println(len(a), cap(a)) //2 2
      a = append(a, 2, 3, 4)
      fmt.Println(len(a), cap(a)) // 5 6
      

      按照规则1,这时的cap应该是5,结果是6。

      a := make([]int, 1280, 1280)
      fmt.Println(len(a), cap(a)) //1280 1280
      a = append(a, 1)
      fmt.Println(len(a), cap(a), 1280*1.25) //1281 1696 1600
      

      按照规则3,这时的cap应该是原来的1.25倍,即1600,结果是1696。

      内存对齐

      其实上面两个扩容,只能说不是最终的结果,go还会做一些内存对齐的优化,通过内存对齐可以提升读取的效率。

      // 内存对齐
      capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
      capmem = roundupsize(capmem)
      newcap = int(capmem / et.size)
      

      空切片和nil切片

      空切片:slice的指针不为空,len和cap都是0
      nil切片:slice的指针不指向任何地址即array=0,len和cap都是0

      nil
      var a []int a:=make([]int,0)
      a:=*new([]int) a:=[]int{}

      空切片虽然地址不为空,但是这个地址也不代表任何底层数组的地址,空切片在初始化的时候会指向一个叫做zerobase的地址,

      var zerobase uintptr
      if size == 0 {
            return unsafe.Pointer(&zerobase)
      }
      

      所有空切片的地址都是一样的。

      var a1 []int
      a2:=*new([]int)
      a3:=make([]int,0)
      a4:=[]int{}
      
      fmt.Println(*(*[3]int)(unsafe.Pointer(&a1))) //[0 0 0]
      fmt.Println(*(*[3]int)(unsafe.Pointer(&a2))) //[0 0 0]
      fmt.Println(*(*[3]int)(unsafe.Pointer(&a3))) //[824634101440 0 0]
      fmt.Println(*(*[3]int)(unsafe.Pointer(&a4))) //[824634101440 0 0]
      

      数组是值传递,切片是引用传递?

      func main() {
         array := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
         slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
         changeArray(array)
         fmt.Println(array) //[0 1 2 3 4 5 6 7 8 9]
         changeSlice(slice)
         fmt.Println(slice) //[1 1 2 3 4 5 6 7 8 9]
      }
      
      func changeArray(a [10]int) {
         a[0] = 1
      }
      
      func changeSlice(a []int) {
         a[0] = 1
      }
      
      • 定义一个数组和一个切片
      • 通过changeArray改变数组下标为0的值
      • 通过changeSlice改变切片下标为0的值
      • 原数组值未被修改,原切片的值已经被修改 这个表象看起来像是slice是指针传递似的,但是如果我们这样呢:
      func main() {
         slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
         changeSlice(slice)//[0 1 2 3 4 5 6 7 8 9]
      }
      func changeSlice(a []int) {
         a = append(a, 99)
      }
      

      会发现原slice的值并没有被改变,这是因为我们用了append,append之后,原slice的容量已经不够了,这时候会copy出一个新的数组。其实go的函数参数传递,只有值传递,没有引用传递,当slice的底层数据没有改变的时候,怎么修改都会影响原底层数组,当slice发生扩容时,扩容后就是新的数组,那么怎么修改这个新的数组都不会影响原来的数组。

      数组和slice能不能比较

      只有长度相同,类型也相同的数组才能比较

      a:=[2]int{1,2}
      b:=[2]int{1,2}
      fmt.Println(a==b) true
      
      a:=[2]int{1,2}
      b:=[3]int{1,2,3}
      fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [3]int)
      
      a:=[2]int{1,2}
      b:=[2]int8{1,2}
      fmt.Println(a==b) //invalid operation: a == b (mismatched types [2]int and [2]int8)
      

      slice只能和nil做比较,其余的都不能比较

      a:=[]int{1,2}
      b:=[]int{1,2}
      fmt.Println(a==b)//invalid operation: a == b (slice can only be compared to nil)
      

      但是需要注意的是,两个都是nil的slice也不能进行比较,它只能和nil对比,这里的nil是真真实实的nil。

      var a []int
      var b []int
      fmt.Println(a == b) //invalid operation: a == b (slice can only be compared to nil)
      fmt.Println(a == nil) //true
      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。