go-slice源码分析(slice引用类型的坑)

GGBond_8488 · · 1433 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

在写代码的时候无意中遇到了一个让我很惊讶的情况

var slices := make([]int,1,3)
fmt.Printf("before append  a:%v  ptr_a%p  &a:%p  len:%d\n",slices,slices,&slices,len(slices))
TryAppend(slices)
fmt.Printf("after append a:%v  ptr_a%p  &a:%p  len:%d\n",slices,slices,&slices,len(slices))

func TryAppend(a []int){
    a[0] = 2
    fmt.Printf("before inner append  a:%v  ptr_a:%p   &a:%p   &a[0]%p   len:%d \n",a,a,&a,&a[0],len(a))
    p := append(a,1)
    fmt.Printf("after inner append new p:%v   ptr_p:%p   &p:%p   &p[0]%p len:%d\n",p,p,&p,&p[0],len(p))

}

之前我一直是这么以为的slice是引用类型,它在传值的时候会直接修改自身,所以我认为上面的程序运行结果应该是(不看TryAppend函数里的打印,以及指针的打印)

a:[0]  len:1 这是初始化的结果
a:[2,1] len:2 这是调用了TryAppend以后的结果

但是事实呢?

before append a:[0]  ptr_a0xc000010340  &a:0xc0000044a0  len:1
before inner append  a:[2] ptr_a:0xc000010340 &a:0xc000004520 &a[0]0xc000010340 len:1 
after inner append new p:[2 1] ptr_p:0xc000010340 &p:0xc000004580 &p[0]0xc000010340  len:2
after append a:[2]  ptr_a0xc000010340  &a:0xc0000044a0  len:1

我们挑出TryAppend函数外得输出

before append a:[0]  ptr_a0xc000010340  &a:0xc0000044a0  len:1
after append a:[2]  ptr_a0xc000010340  &a:0xc0000044a0  len:1

奇怪的是,第一个元素的值修改了,但是append函数似乎没作用到slices上?所以slice到底是引用类型还是仅仅是copy值得值类型呢?

再来看看TryAppend函数内部得结果

p := append(a,1)

before inner append       a:[2] ptr_a:0xc000010340 &a:0xc000004520 &a[0]0xc000010340 len:1 
after inner append new    p:[2 1] ptr_p:0xc000010340 &p:0xc000004580 &p[0]0xc000010340  len:2

可能有人会说 我改成

a = append(a,1)

是不是就不会有这个问题了?append的结果也会被应用到main里的slices
大家可以去试试,这里为了分析,就取另外一个名字比较好说明。
但是大家可以看到 a与p指向的底层数组是一样的 因为他们的首地址是一样的(这里没有超出容量,所以并没有扩容,因此并不是把值copy到了新的底层数组
我们再来打印一下p第二个元素的地址

after inner append new    p:[2 1] ptr_p:0xc000010340 &p:0xc000004580 &p[0]0xc000010340 &p[0]0xc000010348 len:2

用340-348很对,因为int占8个字节嘛(一般说来)

int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32.
int 是带符号整数类型,其大小至少为32位。 它是一种确切的类型,而不是 int32 的别名。
int 不是int32,那 int 在内存占多少字节呢?自己测试一下吧

回到slice的问题,p[0]与p[1]可以看到是连续的而且也是与slices指向的同一个地址,那为什么它就是没被修改呢?

slice大小

先来看一份代码


package main
import (
  "fmt"
  "unsafe"
)
func main() {
  var a int
  var b int8
  var c int16
  var d int32
  var e int64
  slice := make([]int, 0)
  slice = append(slice, 1)
  fmt.Printf("int:%d\nint8:%d\nint16:%d\nint32:%d\nint64:%d\n", unsafe.Sizeof(a), unsafe.Sizeof(b), unsafe.Sizeof(c), unsafe.Sizeof(d), unsafe.Sizeof(e))
  fmt.Printf("slice:%d", unsafe.Sizeof(slice))
}
int:8
int8:1
int16:2
int32:4
int64:8
slice:24

可以看到slice本身大小24个字节

为什么会占24byte,这就跟slice底层定义的结构有关,我们在golang的runtime/slice.go中可以找到slice的结构定义,如下:

type slice struct {
  array unsafe.Pointer//指向底层数组的指针
  len   int//切片的长度
  cap   int//切片的容量
}

我们可以看到slice中定义了三个变量,一个是指向底层数字的指针array,另外两个是切片的长度len和切片的容量cap。

slice初始化
package main
import "fmt"
func main() {
  slice := make([]int, 0)
  slice = append(slice, 1)
  fmt.Println(slice, len(slice), cap(slice))
}

很简单的一段代码,make一个slice,往slice中append一个一个1,打印slice内容,长度和容量,接下来我们利用gotool提供的工具将以上代码反汇编:

go tool compile -S StudySlice.go

会得到一大堆汇编代码,我们主要关心其中的这几个语句

0x0049 00073 (StudySlice.go:5)  CALL    runtime.makeslice(SB)
0x0074 00116 (StudySlice.go:6)  CALL    runtime.growslice(SB)
0x00ab 00171 (StudySlice.go:7)  CALL    runtime.convTslice(SB)
0x00c3 00195 (StudySlice.go:7)  CALL    runtime.convT64(SB)
0x00db 00219 (StudySlice.go:7)  CALL    runtime.convT64(SB)

可以看到,底层是调用runtime中的makeslice方法来创建slice的,我们来看一下makeslice函数到底做了什么?

//makeslice源码  src/runtime/slice.go   line:34
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: Produce a 'len out of range' error instead of a
        // 'cap out of range' error when someone does make([]T, bignumber).
        // 'cap out of range' is true too, but since the cap is only being
        // supplied implicitly, saying len is clearer.
        // See golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }

    return mallocgc(mem, et, true)
}
// MulUintptr源码   src/runtime/internal/math/math.go
const MaxUintptr = ^uintptr(0)

// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
    if a|b < 1<<(4*sys.PtrSize) || a == 0 {
        return a * b, false
    }
    overflow := b > MaxUintptr/a
    return a * b, overflow
}

简单来说,makeslice函数的工作主要就是计算slice所需内存大小,然后调用mallocgc进行内存的分配。计算slice所需内存又是通过MulUintptr来实现的,MulUintptr的源码我也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,如果内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外如果传入长度小于0或者长度小于容量,makeslice也会报错

slice的append操作

基础的扩容就不说了都知道,但是扩容的机制呢
我们看到之前汇编代码中也有调用了一个growslice函数,我们来看看这个代码

func growslice(et *_type, old slice, cap int) slice {
  ...
  ...
  if cap < old.cap {
    panic(errorString("growslice: cap out of range"))
  }
  if et.size == 0 {
    // append should not create a slice with nil pointer but non-zero len.
    // We assume that append doesn't need to preserve old.array in this case.
    return slice{unsafe.Pointer(&zerobase), old.len, cap}
  }
  newcap := old.cap//1280
  doublecap := newcap + newcap//1280+1280=2560
  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//1280*1.25=1600
      }
      // Set newcap to the requested cap when
      // the newcap calculation overflowed.
      if newcap <= 0 {
        newcap = cap
      }
    }
  }
  ...
}

注意到其实扩容不是我们简单的理解的直接扩充两倍,在超过了1024以后其实是以原来的1.25倍增长的
如果你在探究下去你会发现后面居然也不是1.25倍扩增了?(这里就不在讨论了,这涉及到了go的内存分配问题,有兴趣的可以后面自己取探究一下)

append消失问题

回到我们最初提到的问题
前面我们说到slice底层其实是一个结构体,len、cap、array分别表示长度、容量、底层数组的地址,当slice作为函数的参数传递的时候,跟普通结构体的传递是没有区别的;如果直接传slice,实参slice是不会被函数中的操作改变的,但是如果传递的是slice的指针,是会改变原来的slice的;另外,无论是传递slice还是slice的指针,如果改变了slice的底层数组,那么都是会影响slice的,这种通过数组下标的方式更新slice数据,是会对底层数组进行改变的,所以就会影响slice。

那么,讲到这里,在第一段程序中在TryAppend函数内append的1到哪里去了,不可能凭空消失啊,我们再来看一段程序:

func main() {
    var slices =make([]int,1,3)
    slice2 := slices[0:3]
    fmt.Printf("before append slices:%v %d\n",slices,len(slices))
    TryAppend(slices)
    (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 2
    fmt.Printf("after append slices:%v %d\n",slices,len(slices))
    fmt.Printf("after append slice2:%v %d\n",slice2,len(slice2))
}
func TryAppend(a []int){
    a[0] = 2
    //fmt.Printf("before inner append       a:%v ptr_a:%p &a:%p &a[0]%p len:%d \n",a,a,&a,&a[0],len(a))
    a = append(a,1)
    //fmt.Printf("after inner append new    p:%v ptr_p:%p &p:%p &p[0]%p &p[0]%p len:%d\n",p,p,&p,&p[0],&p[1],len(p))
}
before append slices:[0] 1
after append slices:[2] 1
after append slice2:[2 1 0] 3

会发现append的1又回来了
显然,虽然在append后,slices中并未展示出1,也无法通过slice[1]取到(会数组越界),但是实际上底层数组已经有了1这个元素,但是由于slices的len未发生改变,所以我们在上层是无法获取到1这个元素的。那么,再问一个问题,我们是不是可以手动强制改变slice的len长度,让我们可以获取到1这个元素呢?是可以的,我们来看一段程序:

func main() {
    var slices =make([]int,1,3)
    slice2 := slices[0:3]
    fmt.Printf("before append slices:%v %d\n",slices,len(slices))
    TryAppend(slices)
    (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 2
    fmt.Printf("after append slices:%v %d\n",slices,len(slices))
    fmt.Printf("after append slice2:%v %d\n",slice2,len(slice2))
}
func TryAppend(a []int){
    a[0] = 2
    //fmt.Printf("before inner append       a:%v ptr_a:%p &a:%p &a[0]%p len:%d \n",a,a,&a,&a[0],len(a))
    a = append(a,1)
    //fmt.Printf("after inner append new    p:%v ptr_p:%p &p:%p &p[0]%p &p[0]%p len:%d\n",p,p,&p,&p[0],&p[1],len(p))
}
before append slices:[0] 1
after append slices:[2 1] 2
after append slice2:[2 1 0] 3
参考:https://mp.weixin.qq.com/s/Hl6qExD0PSA29ZoaD4WitA

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:GGBond_8488

查看原文:go-slice源码分析(slice引用类型的坑)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1433 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传