Go的Slice注意事项Golangslice知识点

Golang slice知识点

Golang slice内容

初始化方法的区别

var s [] int // s会被初始化为一个nil
s := []int{} // s会被初始化为一个size为0的切片
s := make([]int, 初始化长度) // s会被零填充,后续使用append方法的话会在初始化长度后面进行添加
复制代码

切片自动扩容方法

/**
 slice切片自动扩容存在一些奇怪的问题,
简单来说就是他的增大容量和ratio不是一个简单的固定比例或者固定值,
而是一个不定长的数据,推测可能和相关的原始slice相关
测试版本go version go1.15.13 linux/amd64
测试代码如下:
*/
package main
import (
"fmt"
)
func main() {
   s := make([]int, 0)
   oldCap := cap(s)
   for i := 0; i < 1024*64; i++ {
      s = append(s, i)
      newCap := cap(s)
      if newCap != oldCap {
         fmt.Printf("before cap = %-4d  |  after append %-4d  cap = %-4d  raise = %-4d ratio = %.4f\n",  oldCap, i, newCap, newCap - oldCap,  float64(newCap)/float64(oldCap))
         oldCap = newCap
      }
   }
}
复制代码

测试结果如下:

before cap = 0     |  after append 0     cap = 1     raise = 1    ratio = +Inf
before cap = 1     |  after append 1     cap = 2     raise = 1    ratio = 2.0000
before cap = 2     |  after append 2     cap = 4     raise = 2    ratio = 2.0000
before cap = 4     |  after append 4     cap = 8     raise = 4    ratio = 2.0000
before cap = 8     |  after append 8     cap = 16    raise = 8    ratio = 2.0000
before cap = 16    |  after append 16    cap = 32    raise = 16   ratio = 2.0000
before cap = 32    |  after append 32    cap = 64    raise = 32   ratio = 2.0000
before cap = 64    |  after append 64    cap = 128   raise = 64   ratio = 2.0000
before cap = 128   |  after append 128   cap = 256   raise = 128  ratio = 2.0000
before cap = 256   |  after append 256   cap = 512   raise = 256  ratio = 2.0000
before cap = 512   |  after append 512   cap = 1024  raise = 512  ratio = 2.0000
before cap = 1024  |  after append 1024  cap = 1280  raise = 256  ratio = 1.2500
before cap = 1280  |  after append 1280  cap = 1696  raise = 416  ratio = 1.3250
before cap = 1696  |  after append 1696  cap = 2304  raise = 608  ratio = 1.3585
before cap = 2304  |  after append 2304  cap = 3072  raise = 768  ratio = 1.3333
before cap = 3072  |  after append 3072  cap = 4096  raise = 1024 ratio = 1.3333
before cap = 4096  |  after append 4096  cap = 5120  raise = 1024 ratio = 1.2500
before cap = 5120  |  after append 5120  cap = 7168  raise = 2048 ratio = 1.4000
before cap = 7168  |  after append 7168  cap = 9216  raise = 2048 ratio = 1.2857
before cap = 9216  |  after append 9216  cap = 12288  raise = 3072 ratio = 1.3333
before cap = 12288  |  after append 12288  cap = 15360  raise = 3072 ratio = 1.2500
before cap = 15360  |  after append 15360  cap = 19456  raise = 4096 ratio = 1.2667
before cap = 19456  |  after append 19456  cap = 24576  raise = 5120 ratio = 1.2632
before cap = 24576  |  after append 24576  cap = 30720  raise = 6144 ratio = 1.2500
before cap = 30720  |  after append 30720  cap = 38912  raise = 8192 ratio = 1.2667
before cap = 38912  |  after append 38912  cap = 49152  raise = 10240 ratio = 1.2632
before cap = 49152  |  after append 49152  cap = 61440  raise = 12288 ratio = 1.2500
复制代码

查阅源码之后发现实际上的容量调整方式表现为当原数组大小小于1024时候直接扩容两倍,否则一直增加0.25

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
        }
    }
}
复制代码

之后会根据slice中的数据类型的模式,向上取整计算新容量所需要的内存,并在此基础上修改容量,其代码如下:

var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
    lenmem = uintptr(old.len)
    newlenmem = uintptr(cap)
    capmem = roundupsize(uintptr(newcap))
    overflow = uintptr(newcap) > maxAlloc
    newcap = int(capmem)
case et.size == sys.PtrSize:
    lenmem = uintptr(old.len) * sys.PtrSize
    newlenmem = uintptr(cap) * sys.PtrSize
    capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
    overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
    newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
    var shift uintptr
    if sys.PtrSize == 8 {
        // Mask shift for better code generation.
        shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
    } else {
        shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
    }
    lenmem = uintptr(old.len) << shift
    newlenmem = uintptr(cap) << shift
    capmem = roundupsize(uintptr(newcap) << shift)
    overflow = uintptr(newcap) > (maxAlloc >> shift)
    newcap = int(capmem >> shift)
default:
    lenmem = uintptr(old.len) * et.size
    newlenmem = uintptr(cap) * et.size
    capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
    capmem = roundupsize(capmem)
    newcap = int(capmem / et.size)
    }
复制代码

所以上述扩容过程就存在很多的1.33这种比例,这个问题其实是由于后续的扩容导致的问题。

使用slice作为函数参数的问题

使用slice作为函数参数非常方便,并且能够避免数组类型值传递导致的原始数据未发生改变的问题,但是同时也需要注意扩容在slice函数中的问题。由于slice本质上是一个指针和len,cap形成的数据体。

// slice 类型本质
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
复制代码

在扩容过程中传入的slice和传出的slice地址位置如果发生变化的话会导致原始slice未发生变化,新的slice在下一轮回收中被回收掉。测试代码和测试结果如下:

package main
import (
   "fmt"
)

func ModifySlice(slice [] int, i int)[] int {
   slice[0] = 1
   // 输出append前的地址位置和结果
   fmt.Printf("before append: slice at %p, slice is ", &slice )
   fmt.Println(slice)
   slice = append(slice, i)
   fmt.Printf("after append: slice at %p, slice is ", &slice )
   fmt.Println(slice)
   return  slice
}

func main()  {
   s := make([] int, 3)
   for i := 0; i < 10; i++{
      ModifySlice(s,i )
      // 输出函数外前的地址位置和结果
      fmt.Printf("out of function: slice at %p, slice is", &s )
      fmt.Println(s)
   }
}
复制代码
before append: slice at 0xc0000ae040, slice is [1 0 0]
after append: slice at 0xc0000ae040, slice is [1 0 0 0]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae0c0, slice is [1 0 0]
after append: slice at 0xc0000ae0c0, slice is [1 0 0 1]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae140, slice is [1 0 0]
after append: slice at 0xc0000ae140, slice is [1 0 0 2]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae1c0, slice is [1 0 0]
after append: slice at 0xc0000ae1c0, slice is [1 0 0 3]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae240, slice is [1 0 0]
after append: slice at 0xc0000ae240, slice is [1 0 0 4]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae2c0, slice is [1 0 0]
after append: slice at 0xc0000ae2c0, slice is [1 0 0 5]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae340, slice is [1 0 0]
after append: slice at 0xc0000ae340, slice is [1 0 0 6]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae3c0, slice is [1 0 0]
after append: slice at 0xc0000ae3c0, slice is [1 0 0 7]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae440, slice is [1 0 0]
after append: slice at 0xc0000ae440, slice is [1 0 0 8]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
before append: slice at 0xc0000ae4c0, slice is [1 0 0]
after append: slice at 0xc0000ae4c0, slice is [1 0 0 9]
out of function: slice at 0xc0000ae020, slice is[1 0 0]
复制代码

可以看出首先slice数据类型参数传递实际传递的是一个地址的引用,而append方法如果发生扩容会导致slice的地址变化。这一点会导致生产中的一些改动没有生效。
这一点其实本质上是Golang都是值传递,没有引用传递的说法,也就是如果传递的是一个slice的指针实际上是不会有什么问题的,但是如果传递的是一个slice值。