乐趣区

关于golang:golangslice源码分析

首先咱们来看一段代码:

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:%dnint8:%dnint16:%dnint32:%dnint64:%dn", unsafe.Sizeof(a), unsafe.Sizeof(b), unsafe.Sizeof(c), unsafe.Sizeof(d), unsafe.Sizeof(e))
  fmt.Printf("slice:%d", unsafe.Sizeof(slice))
}

该程序输入 golang 中罕用数据类型占多少 byte,输入后果是:

int:8
int8:1
int16:2
int32:4
int64:8
slice:24

咱们能够看到 slice 占 24byte,为什么会占 24byte,这就跟 slice 底层定义的构造无关,咱们在 golang 的 runtime/slice.go 中能够找到 slice 的构造定义,如下:

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

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

slice 初始化


简略理解了 slice 的底层构造后,咱们来看下 slice 的初始化,在 golang 中 slice 有多重初始化形式,在这里咱们就不一一介绍了,咱们次要关注 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 slice.go

失去汇编代码如下(截取局部):

0x0000 00000 (slice.go:8)  TEXT  "".main(SB), ABIInternal, $152-0
  0x0000 00000 (slice.go:8)  MOVQ  (TLS), CX
  0x0009 00009 (slice.go:8)  LEAQ  -24(SP), AX
  0x000e 00014 (slice.go:8)  CMPQ  AX, 16(CX)
  0x0012 00018 (slice.go:8)  JLS  375
  0x0018 00024 (slice.go:8)  SUBQ  $152, SP
  0x001f 00031 (slice.go:8)  MOVQ  BP, 144(SP)
  0x0027 00039 (slice.go:8)  LEAQ  144(SP), BP
  0x002f 00047 (slice.go:8)  FUNCDATA  $0, gclocals- f14a5bc6d08bc46424827f54d2e3f8ed(SB)// 编译器产生,用于保留一些垃圾收集相干的信息
  0x002f 00047 (slice.go:8)  FUNCDATA  $1, gclocals- 3e7bd269c75edba02eda3b9069a96409(SB)
  0x002f 00047 (slice.go:8)  FUNCDATA  $2, gclocals- f6aec3988379d2bd21c69c093370a150(SB)
  0x002f 00047 (slice.go:8)  FUNCDATA  $3, "".main.stkobj(SB)
  0x002f 00047 (slice.go:9)  PCDATA  $0, $1
  0x002f 00047 (slice.go:9)  PCDATA  $1, $0
  0x002f 00047 (slice.go:9)  LEAQ  type.int(SB), AX
  0x0036 00054 (slice.go:9)  PCDATA  $0, $0
  0x0036 00054 (slice.go:9)  MOVQ  AX, (SP)
  0x003a 00058 (slice.go:9)  XORPS  X0, X0
  0x003d 00061 (slice.go:9)  MOVUPS  X0, 8(SP)
  0x0042 00066 (slice.go:9)  CALL  runtime.makeslice(SB)// 初始化 slice
  0x0047 00071 (slice.go:9)  PCDATA  $0, $1
  0x0047 00071 (slice.go:9)  MOVQ  24(SP), AX
  0x004c 00076 (slice.go:10)  PCDATA  $0, $2
  0x004c 00076 (slice.go:10)  LEAQ  type.int(SB), CX
  0x0053 00083 (slice.go:10)  PCDATA  $0, $1
  0x0053 00083 (slice.go:10)  MOVQ  CX, (SP)
  0x0057 00087 (slice.go:10)  PCDATA  $0, $0
  0x0057 00087 (slice.go:10)  MOVQ  AX, 8(SP)
  0x005c 00092 (slice.go:10)  XORPS  X0, X0
  0x005f 00095 (slice.go:10)  MOVUPS  X0, 16(SP)
  0x0064 00100 (slice.go:10)  MOVQ  $1, 32(SP)
  0x006d 00109 (slice.go:10)  CALL  runtime.growslice(SB)//append 操作
  0x0072 00114 (slice.go:10)  PCDATA  $0, $1
  0x0072 00114 (slice.go:10)  MOVQ  40(SP), AX
  0x0077 00119 (slice.go:10)  MOVQ  48(SP), CX
  0x007c 00124 (slice.go:10)  MOVQ  56(SP), DX
  0x0081 00129 (slice.go:10)  MOVQ  DX, "".slice.cap+72(SP)
  0x0086 00134 (slice.go:10)  MOVQ  $1, (AX)
  0x008d 00141 (slice.go:11)  PCDATA  $0, $0
  0x008d 00141 (slice.go:11)  MOVQ  AX, (SP)
  0x0091 00145 (slice.go:10)  LEAQ  1(CX), AX
  0x0095 00149 (slice.go:10)  MOVQ  AX, "".slice.len+64(SP)
  0x009a 00154 (slice.go:11)  MOVQ  AX, 8(SP)
  0x009f 00159 (slice.go:11)  MOVQ  DX, 16(SP)
  0x00a4 00164 (slice.go:11)  CALL  runtime.convTslice(SB)// 类型转换
  0x00a9 00169 (slice.go:11)  PCDATA  $0, $1
  0x00a9 00169 (slice.go:11)  MOVQ  24(SP), AX
  0x00ae 00174 (slice.go:11)  PCDATA  $0, $0
  0x00ae 00174 (slice.go:11)  PCDATA  $1, $1
  0x00ae 00174 (slice.go:11)  MOVQ  AX, ""..autotmp_33+88(SP)
  0x00b3 00179 (slice.go:11)  MOVQ  "".slice.len+64(SP), CX
  0x00b8 00184 (slice.go:11)  MOVQ  CX, (SP)
  0x00bc 00188 (slice.go:11)  CALL  runtime.convT64(SB)
  0x00c1 00193 (slice.go:11)  PCDATA  $0, $1
  0x00c1 00193 (slice.go:11)  MOVQ  8(SP), AX
  0x00c6 00198 (slice.go:11)  PCDATA  $0, $0
  0x00c6 00198 (slice.go:11)  PCDATA  $1, $2
  0x00c6 00198 (slice.go:11)  MOVQ  AX, ""..autotmp_34+80(SP)
  0x00cb 00203 (slice.go:11)  MOVQ  "".slice.cap+72(SP), CX
  0x00d0 00208 (slice.go:11)  MOVQ  CX, (SP)
  0x00d4 00212 (slice.go:11)  CALL  runtime.convT64(SB)
  0x00d9 00217 (slice.go:11)  PCDATA  $0, $1
  0x00d9 00217 (slice.go:11)  MOVQ  8(SP), AX
  0x00de 00222 (slice.go:11)  PCDATA  $1, $3
  0x00de 00222 (slice.go:11)  XORPS  X0, X0

大家可能看到这里有点蒙,这是在干啥,其实咱们只须要关注一些要害的信息就好了,次要是这几行:

0x0042 00066 (slice.go:9)  CALL  runtime.makeslice(SB)// 初始化 slice
0x006d 00109 (slice.go:10)  CALL  runtime.growslice(SB)//append 操作
0x00a4 00164 (slice.go:11)  CALL  runtime.convTslice(SB)// 类型转换
0x00bc 00188 (slice.go:11)  CALL  runtime.convT64(SB)
0x00d4 00212 (slice.go:11)  CALL  runtime.convT64(SB)

咱们能察看出,底层是调用 runtime 中的 makeslice 办法来创立 slice 的,咱们来看一下 makeslice 函数到底做了什么。

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()}

  // Allocate an object of size bytes.
  // Small objects are allocated from the per-P cache's free lists.
  // Large objects (> 32 kB) are allocated straight from the heap.
  return mallocgc(mem, et, true)
}
func panicmakeslicelen() {panic(errorString("makeslice: len out of range"))
}
func panicmakeslicecap() {panic(errorString("makeslice: cap out of range"))
}

MulUintptr 函数源码:

package math
import "runtime/internal/sys"
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 {//a|b < 1<<(4*8)
    return a * b, false
  }
  overflow := b > MaxUintptr/a
  return a * b, overflow
}

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

append 操作


首先咱们来看一段程序:

package main

import (
   "fmt"
   "unsafe"
)

func main() {slice := make([]int, 0, 10)
   slice = append(slice, 1)
   fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice))
   slice = append(slice, 2)
   fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice))
}

咱们间接给出后果:

0xc00009e000 1 10
0xc00009e000 2 10

咱们能够看到,当 slice 容量足够时,咱们往 slice 中 append 一个 2,slice 底层数组指向的内存地址没有产生扭转;再看一段程序:

func main() {slice := make([]int, 0)
   slice = append(slice, 1)
   fmt.Printf("%p %d %dn", unsafe.Pointer(&slice[0]), len(slice), cap(slice))
   slice = append(slice, 2)
   fmt.Printf("%p %d %dn", unsafe.Pointer(&slice[0]), len(slice), cap(slice))
}

输入后果是:

0xc00009a008 1 1
0xc00009a030  2 2

咱们能够看到当往 slice 中 append 一个 1 后,slice 底层数组的指针指向地址 0xc00009a008,长度为 1,容量为 1。这时再往 slice 中 append 一个 2,那么 slice 的容量不够了,此时底层数组会产生 copy,会重新分配一块新的内存地址,容量也变成了 2,所以咱们会看到底层数组的指针指向地址产生了扭转。依据之前汇编的后果咱们通晓了,append 操作其实是调用了 runtime/slice.go 中的 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}
    }
  }
  ...
}

咱们次要关注下 cap 的扩容规定,从源码中咱们能够简略的总结出 slice 容量的扩容规定:当原 slice 的 cap 小于 1024 时,新 slice 的 cap 变为原来的 2 倍;原 slice 的 cap 大于 1024 时,新 slice 变为原来的 1.25 倍,咱们写个程序来验证下:

package main
import "fmt"
func main() {slice := make([]int, 0)
  oldCap := cap(slice)
  for i := 0; i < 4096; i++ {slice = append(slice, i)
    newCap := cap(slice)
    if newCap != oldCap {fmt.Printf("oldCap = %-4d  after append %-4d  newCap = %-4dn", oldCap, i, newCap)
      oldCap = newCap
    }
  }
}

这段程序实现的性能是:当 cap 产生扭转时,打印出 cap 扭转前后的值。咱们来看程序的输入后果:

oldCap = 0     after append 0     newCap = 1   
oldCap = 1     after append 1     newCap = 2   
oldCap = 2     after append 2     newCap = 4   
oldCap = 4     after append 4     newCap = 8   
oldCap = 8     after append 8     newCap = 16  
oldCap = 16    after append 16    newCap = 32  
oldCap = 32    after append 32    newCap = 64  
oldCap = 64    after append 64    newCap = 128 
oldCap = 128   after append 128   newCap = 256 
oldCap = 256   after append 256   newCap = 512 
oldCap = 512   after append 512   newCap = 1024
oldCap = 1024  after append 1024  newCap = 1280
oldCap = 1280  after append 1280  newCap = 1696
oldCap = 1696  after append 1696  newCap = 2304
oldCap = 2304  after append 2304  newCap = 3072
oldCap = 3072  after append 3072  newCap = 4096

一开始的时候看起来跟我说的扩容规定是一样的,从 1 ->2->4->8->16…->1024,都是成倍增长,当 cap 大于 1024 后,再 append 元素,cap 变为 1280,变成了 1024 的 1.25 倍,也合乎咱们的规定;然而持续 append,1280->1696,仿佛不是 1.25 倍,而是 1.325 倍,可见扩容规定并不是咱们以上所说的那么简略,咱们再持续往下看源码:

 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)//13568
    overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
    newcap = int(capmem / sys.PtrSize)//13568/8=1696
  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)
  }

咱们看到每个 case 中都执行了 roundupsize,咱们再看下 roundupsize 的源码,如下:

package runtime
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
  if size < _MaxSmallSize {//size=1600*8=12800<32768
    if size <= smallSizeMax-8 {//12800<=0
      return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
    } else {return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])//size_to_class128[92]= 56
      //class_to_size[56]=13568
      //13568/8=1696
    }
  }
  if size+_PageSize < size {return size}
  return round(size, _PageSize)
}
const _MaxSmallSize   = 32768
const  smallSizeDiv    = 8
const  smallSizeMax    = 1024
const largeSizeDiv    = 128

其实 roundupsize 是内存对齐的过程,咱们晓得 golang 中内存调配是依据对象大小来配不同的 mspan,为了防止造成过多的内存碎片,slice 在扩容中须要对扩容后的 cap 容量进行内存对齐的操作,接下来咱们对照源码来理论计算下 cap 容量是否由 1280 变成了 1696。

从以上流程图能够看出,cap 在变成 1600 后又进入了内存对齐的过程,最终 cap 变为了 1696。

slice 截取


go 中的 slice 是反对截取操作的,尽管应用起来十分的不便,然而有很多坑,稍有不慎就会呈现 bug 且不易排查。

让咱们来看一段程序:

package main

import "fmt"

func main() {slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  s1 := slice[2:5]
  s2 := s1[2:7]
  fmt.Printf("len=%-4d cap=%-4d slice=%-1v n", len(slice), cap(slice), slice)
  fmt.Printf("len=%-4d cap=%-4d s1=%-1v n", len(s1), cap(s1), s1)
  fmt.Printf("len=%-4d cap=%-4d s2=%-1v n", len(s2), cap(s2), s2)
}

程序输入:

len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 9] 
len=3    cap=8    s1=[2 3 4] 
len=5    cap=6    s2=[4 5 6 7 8]

s1 的长度变成 3,cap 变为 8(默认截取到最大容量),然而 s2 截取 s1 的第 2 到第 7 个元素,左闭右开,很多人想问,s1 基本没有那么元素啊,然而理论状况是 s2 截取到了,并且没有产生数组越界,起因就是 s2 理论截取的是底层数组,目前 slice、s1、s2 都是共用的同一个底层数组。

咱们持续操作:

fmt.Println("--------append 100----------------")
s2 = append(s2, 100)

输入后果是:

--------append 100----------------
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100] 
len=3    cap=8    s1=[2 3 4] 
len=6    cap=6    s2=[4 5 6 7 8 100]

咱们看到往 s2 里 append 数据影响到了 slice,正是因为两者底层数组是一样的;然而既然都是共用的同一底层数组,s1 为什么没有 100,这个问题再下一节会讲到,大家稍安勿躁。咱们持续进行操作:

fmt.Println("--------append 200----------------")
s2 = append(s2, 200)

输入后果是:

--------append 200----------------
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100] 
len=3    cap=8    s1=[2 3 4] 
len=7    cap=12   s2=[4 5 6 7 8 100 200]

咱们看到持续往 s2 中 append 一个 200,然而只有 s2 产生了变动,slice 并未扭转,为什么呢?对,是因为在 append 完 100 后,s2 的容量已满,再往 s2 中 append,底层数组产生复制,零碎调配了一块新的内存地址给 s2,s2 的容量也翻倍了。

咱们持续操作:

fmt.Println("--------modify s1----------------")
s1[2] = 20

输入会是什么样呢?

--------modify s1----------------
len=10   cap=10   slice=[0 1 2 3 20 5 6 7 8 100] 
len=3    cap=8    s1=[2 3 20] 
len=7    cap=12   s2=[4 5 6 7 8 100 200]

这就很容易了解了,咱们对 s1 进行更新,影响了 slice,因为两者共用的还是同一底层数组,s2 未产生扭转是因为在上一步时底层数组曾经产生了变动;

以此来看,slice 截取的坑的确很多,极容易呈现 bug,并且难以排查,大家在应用的时候肯定留神。

slice 深拷贝


上一节中对 slice 进行的截取,新的 slice 和原始 slice 共用同一个底层数组,因而能够看做是对 slice 的浅拷贝,那么在 go 中如何实现对 slice 的深拷贝呢?那么就要依赖 golang 提供的 copy 函数了,咱们用一段程序来简略看下如何实现深拷贝:

func main() {

  // Creating slices
  slice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  var slice2 []int
  slice3 := make([]int, 5)

  // Before copying
  fmt.Println("------------before copy-------------")
  fmt.Printf("len=%-4d cap=%-4d slice1=%vn", len(slice1), cap(slice1), slice1)
  fmt.Printf("len=%-4d cap=%-4d slice2=%vn", len(slice2), cap(slice2), slice2)
  fmt.Printf("len=%-4d cap=%-4d slice3=%vn", len(slice3), cap(slice3), slice3)


  // Copying the slices
  copy_1 := copy(slice2, slice1)
  fmt.Println()
  fmt.Printf("len=%-4d cap=%-4d slice1=%vn", len(slice1), cap(slice1), slice1)
  fmt.Printf("len=%-4d cap=%-4d slice2=%vn", len(slice2), cap(slice2), slice2)
  fmt.Println("Total number of elements copied:", copy_1)
}

首先定义了三个 slice,而后将 slice1 copy 到 slice2,咱们来看下输入后果:

------------before copy-------------
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
len=5    cap=5    slice3=[0 0 0 0 0]

len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
Total number of elements copied: 0

咱们发现 slice1 的内容并未 copy 到 slice2,为什么呢?咱们再试下将 slice1 copy 到 slice3,如下:

copy_2 := copy(slice3, slice1)

输入后果:

len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=5    cap=5    slice3=[0 1 2 3 4]
Total number of elements copied: 5

咱们看到 copy 胜利,slice3 和 slice2 惟一的区别就是 slice3 的容量为 5,而 slice2 容量为 0,那么是否是深拷贝呢,咱们批改 slice3 的内容看下:

slice3[0] = 100

咱们再看下输入后果:

len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=5    cap=5    slice3=[100 1 2 3 4]

咱们能够看到批改 slice3 后,slice1 的值并未扭转,可见 copy 实现的是深拷贝。由此可见,copy 函数为 slice 提供了深拷贝能力,然而须要在拷贝前申请内存空间。参照 makeslice 和 growslice 咱们对本节一开始的程序进行反汇编,失去汇编代码(局部)如下:

0x0080 00128 (slice.go:10)  CALL  runtime.makeslice(SB)
  0x0085 00133 (slice.go:10)  PCDATA  $0, $1
  0x0085 00133 (slice.go:10)  MOVQ  24(SP), AX
  0x008a 00138 (slice.go:10)  PCDATA  $1, $2
  0x008a 00138 (slice.go:10)  MOVQ  AX, ""..autotmp_75+96(SP)
  0x008f 00143 (slice.go:11)  PCDATA  $0, $4
  0x008f 00143 (slice.go:11)  MOVQ  ""..autotmp_74+104(SP), CX
  0x0094 00148 (slice.go:11)  CMPQ  AX, CX
  0x0097 00151 (slice.go:11)  JEQ  176
  0x0099 00153 (slice.go:11)  PCDATA  $0, $5
  0x0099 00153 (slice.go:11)  MOVQ  AX, (SP)
  0x009d 00157 (slice.go:11)  PCDATA  $0, $0
  0x009d 00157 (slice.go:11)  MOVQ  CX, 8(SP)
  0x00a2 00162 (slice.go:11)  MOVQ  $40, 16(SP)
  0x00ab 00171 (slice.go:11)  CALL  runtime.memmove(SB)
  0x00b0 00176 (slice.go:12)  MOVQ  $10, (SP)
  0x00b8 00184 (slice.go:12)  CALL  runtime.convT64(SB)

咱们发现 copy 函数其实是调用 runtime.memmove,其实咱们在钻研 runtime/slice.go 文件中的源码的时候,会发现有一个 slicecopy 函数,这个函数最终就是调用 runtime.memmove 来实现 slice 的 copy 的,咱们看下源码:

func slicecopy(to, fm slice, width uintptr) int {
  // 如果源切片或者指标切片有一个长度为 0,那么就不须要拷贝,间接 return 
  if fm.len == 0 || to.len == 0 {return 0}

  // n 记录下源切片或者指标切片较短的那一个的长度
  n := fm.len
  if to.len < n {n = to.len}

  // 如果入参 width = 0,也不须要拷贝了,返回较短的切片的长度
  if width == 0 {return n}

  // 如果开启竞争检测
  if raceenabled {callerpc := getcallerpc()
    pc := funcPC(slicecopy)
    racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
    racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
  }
  if msanenabled {msanwrite(to.array, uintptr(n*int(width)))
    msanread(fm.array, uintptr(n*int(width)))
  }

  size := uintptr(n) * width
  if size == 1 { // common case worth about 2x to do here
    // TODO: is this still worth it with new memmove impl?
    // 如果只有一个元素,那么间接进行地址转换
    *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
  } else {
    // 如果不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址之后,拷贝个数为 size
    memmove(to.array, fm.array, size)
  }
  return n
}

源码解读见中文正文。

值传递还是援用传递


slice 在作为函数参数进行传递的时候,是值传递还是援用传递,咱们来看一段程序:

package main

import "fmt"

func main() {slice := make([]int, 0, 10)
  slice = append(slice, 1)
  fmt.Println(slice, len(slice), cap(slice))
  fn(slice)
  fmt.Println(slice, len(slice), cap(slice))
}
func fn(in []int) {in = append(in, 5)
}

很简略的一段程序,咱们间接来看输入后果:

[1] 1 10
[1] 1 10

可见 fn 内的 append 操作并未对 slice 产生影响,那咱们再看一段代码:

package main

import "fmt"

func main() {slice := make([]int, 0, 10)
  slice = append(slice, 1)
  fmt.Println(slice, len(slice), cap(slice))
  fn(slice)
  fmt.Println(slice, len(slice), cap(slice))
}
func fn(in []int) {in[0] = 100
}

输入是什么?咱们来看下:

[1] 1 10
[100] 1 10

slice 竟然扭转了,是不是有点凌乱?后面咱们说到 slice 底层其实是一个构造体,len、cap、array 别离示意长度、容量、底层数组的地址,当 slice 作为函数的参数传递的时候,跟一般构造体的传递是没有区别的;如果间接传 slice,实参 slice 是不会被函数中的操作扭转的,然而如果传递的是 slice 的指针,是会扭转原来的 slice 的;另外,无论是传递 slice 还是 slice 的指针,如果扭转了 slice 的底层数组,那么都是会影响 slice 的,这种通过数组下标的形式更新 slice 数据,是会对底层数组进行扭转的,所以就会影响 slice。

那么,讲到这里,在第一段程序中在 fn 函数内 append 的 5 到哪里去了,不可能凭空隐没啊,咱们再来看一段程序:

package main

import "fmt"

func main() {slice := make([]int, 0, 10)
  slice = append(slice, 1)
  fmt.Println(slice, len(slice), cap(slice))
  fn(slice)
  fmt.Println(slice, len(slice), cap(slice))
  s1 := slice[0:9]// 数组截取
  fmt.Println(s1, len(s1), cap(s1))
}
func fn(in []int) {in = append(in, 5)
}

咱们来看输入后果:

[1] 1 10
[1] 1 10
[1 5 0 0 0 0 0 0 0] 9 10

显然,尽管在 append 后,slice 中并未展现出 5,也无奈通过 slice[1] 取到(会数组越界), 然而实际上底层数组曾经有了 5 这个元素,然而因为 slice 的 len 未产生扭转,所以咱们在下层是无奈获取到 5 这个元素的。那么,再问一个问题,咱们是不是能够手动强制扭转 slice 的 len 长度,让咱们能够获取到 5 这个元素呢?是能够的,咱们来看一段程序:

package main

import (
  "fmt"
  "reflect"
  "unsafe"
)

func main() {slice := make([]int, 0, 10)
  slice = append(slice, 1)
  fmt.Println(slice, len(slice), cap(slice))
  fn(slice)
  fmt.Println(slice, len(slice), cap(slice))
  (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 2 // 强制批改 slice 长度
  fmt.Println(slice, len(slice), cap(slice))
}

func fn(in []int) {in = append(in, 5)
}

咱们来看输入后果:

[1] 1 10
[1] 1 10
[1 5] 2 10

能够看出,通过强制批改 slice 的 len,咱们能够获取到了 5 这个元素。

所以再次答复一开始咱们提出的问题,slice 是值传递还是援用传递?答案是值传递!

以上,在应用 golang 中的 slice 的时候大家肯定留神,否则稍有不慎就会呈现 bug。

退出移动版