浅谈golang 切片,以及切片扩容

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

1.slice结构
首先来看 slice的结构 第一个array是一个指向底层数组首位的指针,第二个len是数字的长度,也就是数组当前有多少个元素,第三个cap是数组的容量。cap永远会大于等于len

    array unsafe.Pointer
    len   int    // 切片的长度 通过len(s)获取
    cap   int    //  切片的容量 ,通过cap(s)获取
}

2.slice的创建方式

1.使用字面量初始化切片:

s := []int{1, 2, 3, 4, 5}

这种方式直接声明一个切片并通过大括号将元素值初始化。
2.使用make函数创建指定长度和容量的切片:

s := make([]int, 5)

这里的make([]int, 5)创建了一个长度为5的切片,并将其初始化为int类型的零值。
3.使用make函数创建指定长度和容量的切片,并预分配内存空间:

s := make([]int, 5, 10)

这里的make([]int, 5, 10)创建了一个长度为5,容量为10的切片。它会预先分配10个元素大小的内存空间,以便在后续添加元素时可以更高效地操作。
4.创建空切片:

var s []int
s := make([]int, 0)

这里的[]int和make([]int, 0)声明了一个未初始化的空切片。需要注意的是,空切片和nil切片是不同的,空切片是一个已分配但没有任何元素的切片,在操作前需要先分配空间
无论使用哪种方式创建切片,都可以根据需要进行后续的元素添加、删除等操作。

3.slice是值传递
slice在做为参数传递的时候是值传递,列如:

func Test_slice(t *testing.T) {

    s := []int{1,2}
   fmt.Printf("切片的地址:%p\n", &s)
   Zhi(s)
}

func Zhi(s []int) {
   fmt.Printf("切片的地址:%p\n", &s)
}
输出为:
切片的地址:0xc000008138
切片的地址:0xc000008168
可以发现两个数组的地址是不同的,slice在作为参数传递的时候,会拷贝一份传递过去

那这里引入一个问题

func Test_slice(t *testing.T) {

   s1 := []int{1, 2}
   Zhi(s1)
   fmt.Printf("切片的地址:%p\n,%v", &s1, s1)

}

func Zhi(s2 []int) {
   s2[0] = 2
   s2[1] = 1
   fmt.Printf("切片的地址:%p\n,%v", &s2, s2)
}

1.这个时候两个数组打印出的元素会是什么?


切片的地址:0xc000008150
,[2 1]
切片的地址:0xc000008138
,[2 1]
按理来说如果是值传递,那么s1打印的应该是[1,2],s2但是的是[2,1],但是很奇怪s1,s2打印的都是[2,1]
这是因为虽然slice是值传递,但是他是浅拷贝,他拷贝的是slice结构体里面三个字段的值,而第一个字段存的是指向数组首位的指针,在拷贝之后依旧指向的是同一个内存地址
导致两个数组实际上是使用的同一片空间,所以在更改s2的值之后,s1也会发生变化

那么对代码进行些许改动这个时候s1,s2输出的又会是什么

func Test_slice(t *testing.T) {

   s1 := []int{1, 2}
   Zhi(s1)
   fmt.Printf("切片的地址s1:%p\n,%v", &s1, s1)

}

func Zhi(s2 []int) {
   s2 = append(s2, 1)
   s2[0] = 2
   s2[1] = 1
   fmt.Printf("切片的地址s2:%p\n,%v", &s2, s2)
}


切片的地址s2:0xc000008150
,[2 1 1]
切片的地址s1:0xc000008138
,[1 2]
可以发现对s2添加一个元素之后,s2发生了变化,但是s1没有发生变化

4.slice扩容

func Zhi(s2 []int) {
   fmt.Println(cap(s2))
   s2 = append(s2, 1)
   fmt.Println(cap(s2))
   s2[0] = 2
   s2[1] = 1
   fmt.Printf("切片的地址s2:%p\n,%v", &s2, s2)
}

还是上面的代码,在append的前后打印数组的容量,发现数组的容量发生了变化,从2变到了4,扩大了两倍,这是因为这个时候len == cap ,slice没有容量存放新过来的元素,所以s2进行了扩容以便有足够的空间来存放新来的元素。下面是扩容的主要代码:


// runtime/slice.go
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
    newcap := oldCap
    doublecap := newcap + newcap
    if newLen > doublecap {
       newcap = newLen
    } else {
       const threshold = 256
       // 如果 oldCap小于256直接进行翻倍扩容
       if oldCap < threshold {
          newcap = doublecap
       } else {
          // Check 0 < newcap to detect overflow
          // and prevent an infinite loop.
          // 在cap大于256的时候对slice进行扩容
          // 进行循环扩容直接内存分配的内存足够
          for 0 < newcap && newcap < newLen {
             // Transition from growing 2x for small slices
             // to growing 1.25x for large slices. This formula
             // gives a smooth-ish transition between the two.
             // 扩容规则,随着newcap的增加,增长倍数会慢慢的到1.25,使增长变化更加平滑
             newcap += (newcap + 3*threshold) / 4
          }
          // Set newcap to the requested cap when
          // the newcap calculation overflowed.
          if newcap <= 0 {
             newcap = newLen
      }
   }
}
}


s := make([]int, 512)
fmt.Println(cap(s))
s = append(s, 1)
fmt.Println(cap(s))

输出
512
848

但是看这一段代码,如果按照 (newcap + 3*threshold) / 4扩容应该是 832,但是扩容实际到了848,这是因为在计算新切片的容量的时候,会根据切片的元素类型大小来做一些优化。
部分代码为

switch {
case et.Size_ == 1:
   lenmem = uintptr(oldLen)
   newlenmem = uintptr(newLen)
   capmem = roundupsize(uintptr(newcap))
   overflow = uintptr(newcap) > maxAlloc
   newcap = int(capmem)
case et.Size_ == goarch.PtrSize:   // 元素类型是8个字节
   lenmem = uintptr(oldLen) * goarch.PtrSize  
   newlenmem = uintptr(newLen) * goarch.PtrSize
   capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)  // 进行内存优化,也就是内存对齐,获取实际要分配的内存
   overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
   newcap = int(capmem / goarch.PtrSize)   //  计算真正的扩容后容量
case isPowerOfTwo(et.Size_):
   var shift uintptr
   if goarch.PtrSize == 8 {
      // Mask shift for better code generation.
      shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
   } else {
      shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
   }
   lenmem = uintptr(oldLen) << shift
   newlenmem = uintptr(newLen) << shift
   capmem = roundupsize(uintptr(newcap) << shift)
   overflow = uintptr(newcap) > (maxAlloc >> shift)
   newcap = int(capmem >> shift)
   capmem = uintptr(newcap) << shift
default:
   lenmem = uintptr(oldLen) * et.Size_
   newlenmem = uintptr(newLen) * et.Size_
   capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
   capmem = roundupsize(capmem)
   newcap = int(capmem / et.Size_)
   capmem = uintptr(newcap) * et.Size_
}

func roundupsize(size uintptr) uintptr {
   if size < _MaxSmallSize {  // _MaxSmallSize = 32768 也就是4096  当size小于4096时
      if size <= smallSizeMax-8 {  //  smallSizeMax= 1024 也就是当size<=127时
         // size_to_class8数组用来获取class_to_size数组的索引
    // divRoundUp(size, smallSizeDiv),相当于计算ceil(size / 8)
         return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
      } else {
         return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
      }
   }
   if size+_PageSize < size {
      return size
   }
   return alignUp(size, _PageSize)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值