【golang】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:%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,輸出結果是:golang

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

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

獲得彙編代碼以下(截取部分):oop

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

你們可能看到這裏有點蒙,這是在幹啥,其實咱們只須要關注一些關鍵的信息就行了,主要是這幾行:ui

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函數到底作了什麼。this

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函數源碼:spa

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也會報錯。3d

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、s一、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。

相關文章
相關標籤/搜索