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值。




近期评论