欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang中 Slice的分析與使用源碼解析

 更新時(shí)間:2023年03月09日 12:36:55   作者:獨(dú)臂阿童木  
這篇文章主要介紹了Golang 中 Slice的分析與使用(含源碼),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

1、slice結(jié)構(gòu)體

首先我們來看一段代碼

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

該程序輸出golang中常用數(shù)據(jù)類型占多少byte,輸出結(jié)果是

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

我們可以看到slice占24byte,為什么會(huì)占24byte,這就跟slice底層定義的結(jié)構(gòu)有關(guān),我們?cè)趃olang的runtime/slice.go中可以找到slice的結(jié)構(gòu)定義,如下:

type slice struct {
    array unsafe.Pointer//指向底層數(shù)組的指針
    len   int//切片的長(zhǎng)度
    cap   int//切片的容量
}

我們可以看到slice中定義了三個(gè)變量,一個(gè)是指向底層數(shù)字的指針array,另外兩個(gè)是切片的長(zhǎng)度len和切片的容量cap。

2、slice初始化

簡(jiǎn)單了解了slice的底層結(jié)構(gòu)后,我們來看下slice的初始化,在golang中slice有多重初始化方式,在這里我們就不一一介紹了,感興趣的朋友可以自行百度,我們主要關(guān)注slice在底層是如何初始化的,首先我們來看一段代碼

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

很簡(jiǎn)單的一段代碼,make一個(gè)slice,往slice中append一個(gè)一個(gè)1,打印slice內(nèi)容,長(zhǎng)度和容量,接下來我們利用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)//編譯器產(chǎn)生,用于保存一些垃圾收集相關(guān)的信息
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)//類型轉(zhuǎn)換
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

大家可能看到這里有點(diǎn)蒙,這是在干啥,其實(shí)我們只需要關(guān)注一些關(guān)鍵的信息就好了,主要是這幾行

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)//類型轉(zhuǎn)換
0x00bc 00188 (slice.go:11)  CALL    runtime.convT64(SB)
0x00d4 00212 (slice.go:11)  CALL    runtime.convT64(SB)

我們能觀察出,底層是調(diào)用runtime中的makeslice方法來創(chuàng)建slice的,我們來看一下makeslice函數(shù)到底做了什么

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函數(shù)源碼

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
}

簡(jiǎn)單來說,makeslice函數(shù)的工作主要就是計(jì)算slice所需內(nèi)存大小,然后調(diào)用mallocgc進(jìn)行內(nèi)存的分配。計(jì)算slice所需內(nèi)存又是通過MulUintptr來實(shí)現(xiàn)的,MulUintptr的源碼我們也已經(jīng)貼出,主要就是用切片中元素大小和切片的容量相乘計(jì)算出所需占用的內(nèi)存空間,如果內(nèi)存溢出,或者計(jì)算出的內(nèi)存大小大于最大可分配內(nèi)存,MulUintptr的overflow會(huì)返回true,makeslice就會(huì)報(bào)錯(cuò)。另外如果傳入長(zhǎng)度小于0或者長(zhǎng)度小于容量,makeslice也會(huì)報(bào)錯(cuò)。

3、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))
}

我們直接給出結(jié)果

0xc00009e000 1 10
0xc00009e000 2 10

我們可以看到,當(dāng)slice容量足夠時(shí),我們往slice中append一個(gè)2,slice底層數(shù)組指向的內(nèi)存地址沒有發(fā)生改變;再看一段程序

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

輸出結(jié)果是

0xc00009a008 1 1
0xc00009a030 2 2

我們可以看到當(dāng)往slice中append一個(gè)1后,slice底層數(shù)組的指針指向地址0xc00009a008,長(zhǎng)度為1,容量為1。這時(shí)再往slice中append一個(gè)2,那么slice的容量不夠了,此時(shí)底層數(shù)組會(huì)發(fā)生copy,會(huì)重新分配一塊新的內(nèi)存地址,容量也變成了2,所以我們會(huì)看到底層數(shù)組的指針指向地址發(fā)生了改變。根據(jù)之前匯編的結(jié)果我們知曉了,append操作其實(shí)是調(diào)用了runtime/slice.go中的growslice函數(shù),我們來看下源碼:

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
            }
        }
    }
  ...
}

我們主要關(guān)注下cap的擴(kuò)容規(guī)則,從源碼中我們可以簡(jiǎn)單的總結(jié)出slice容量的擴(kuò)容規(guī)則:當(dāng)原slice的cap小于1024時(shí),新slice的cap變?yōu)樵瓉淼?倍;原slice的cap大于1024時(shí),新slice變?yōu)樵瓉淼?.25倍,我們寫個(gè)程序來驗(yàn)證下:

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 = %-4d\n", oldCap, i, newCap)
            oldCap = newCap
        }
    }
}

這段程序?qū)崿F(xiàn)的功能是:當(dāng)cap發(fā)生改變時(shí),打印出cap改變前后的值。我們來看程序的輸出結(jié)果:

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

一開始的時(shí)候看起來跟我說的擴(kuò)容規(guī)則是一樣的,從1->2->4->8->16…->1024,都是成倍增長(zhǎng),當(dāng)cap大于1024后,再append元素,cap變?yōu)?280,變成了1024的1.25倍,也符合我們的規(guī)則;但是繼續(xù)append,1280->1696,似乎不是1.25倍,而是1.325倍,可見擴(kuò)容規(guī)則并不是我們以上所說的那么簡(jiǎn)單,我們?cè)倮^續(xù)往下看源碼:

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

我們看到每個(gè)case中都執(zhí)行了roundupsize,我們?cè)倏聪聄oundupsize的源碼,如下:

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

其實(shí)roundupsize是內(nèi)存對(duì)齊的過程,我們知道golang中內(nèi)存分配是根據(jù)對(duì)象大小來配不同的mspan,為了避免造成過多的內(nèi)存碎片,slice在擴(kuò)容中需要對(duì)擴(kuò)容后的cap容量進(jìn)行內(nèi)存對(duì)齊的操作,接下來我們對(duì)照源碼來實(shí)際計(jì)算下cap容量是否由1280變成了1696

從以上流程圖可以看出,cap在變成1600后又進(jìn)入了內(nèi)存對(duì)齊的過程,最終cap變?yōu)榱?696。

4、slice截取

go中的slice是支持截取操作的,雖然使用起來非常的方便,但是有很多坑,稍有不慎就會(huì)出現(xiàn)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的長(zhǎng)度變成3,cap變?yōu)?(默認(rèn)截取到最大容量), 但是s2截取s1的第2到第7個(gè)元素,左閉右開,很多人想問,s1根本沒有那么元素啊,但是實(shí)際情況是s2截取到了,并且沒有發(fā)生數(shù)組越界,原因就是s2實(shí)際截取的是底層數(shù)組,目前slice、s1、s2都是共用的同一個(gè)底層數(shù)組。我們繼續(xù)操作

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

輸出結(jié)果是:

--------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數(shù)據(jù)影響到了slice,正是因?yàn)閮烧叩讓訑?shù)組是一樣的;但是既然都是共用的同一底層數(shù)組,s1為什么沒有100,這個(gè)問題再下一節(jié)會(huì)講到,大家稍安勿躁。我們繼續(xù)進(jìn)行操作:

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

輸出結(jié)果是:

--------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]

我們看到繼續(xù)往s2中append一個(gè)200,但是只有s2發(fā)生了變化,slice并未改變,為什么呢?對(duì),是因?yàn)樵赼ppend完100后,s2的容量已滿,再往s2中append,底層數(shù)組發(fā)生復(fù)制,系統(tǒng)分配了一塊新的內(nèi)存地址給s2,s2的容量也翻倍了。我們繼續(xù)操作:

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

輸出會(huì)是什么樣呢?

--------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]

這就很容易理解了,我們對(duì)s1進(jìn)行更新,影響了slice,因?yàn)閮烧吖灿玫倪€是同一底層數(shù)組,s2未發(fā)生改變是因?yàn)樵谏弦徊綍r(shí)底層數(shù)組已經(jīng)發(fā)生了變化;

以此來看,slice截取的坑確實(shí)很多,極容易出現(xiàn)bug,并且難以排查,大家在使用的時(shí)候一定注意。

5、slice深拷貝

上一節(jié)中對(duì)slice進(jìn)行的截取,新的slice和原始slice共用同一個(gè)底層數(shù)組,因此可以看做是對(duì)slice的淺拷貝,那么在go中如何實(shí)現(xiàn)對(duì)slice的深拷貝呢?那么就要依賴golang提供的copy函數(shù)了,我們用一段程序來簡(jiǎn)單看下如何實(shí)現(xiàn)深拷貝:

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=%v\n", len(slice1), cap(slice1), slice1)
    fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
    fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3)
 
 
    // Copying the slices
    copy_1 := copy(slice2, slice1)
    fmt.Println()
    fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
    fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
    fmt.Println("Total number of elements copied:", copy_1)
}

首先定義了三個(gè)slice,然后將slice1 copy到slice2,我們來看下輸出結(jié)果:

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

我們發(fā)現(xiàn)slice1的內(nèi)容并未copy到slice2,為什么呢?我們?cè)僭囅聦lice1 copy到slice3,如下:

copy_2 := copy(slice3, slice1)

輸出結(jié)果:

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唯一的區(qū)別就是slice3的容量為5,而slice2容量為0,那么是否是深拷貝呢,我們修改slice3的內(nèi)容看下:

slice3[0] = 100

我們?cè)倏聪螺敵鼋Y(jié)果:

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實(shí)現(xiàn)的是深拷貝。由此可見,copy函數(shù)為slice提供了深拷貝能力,但是需要在拷貝前申請(qǐng)內(nèi)存空間。參照makeslice和growslice我們對(duì)本節(jié)一開始的程序進(jìn)行反匯編,得到匯編代碼(部分)如下:

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)

我們發(fā)現(xiàn)copy函數(shù)其實(shí)是調(diào)用runtime.memmove,其實(shí)我們?cè)谘芯縭untime/slice.go文件中的源碼的時(shí)候,會(huì)發(fā)現(xiàn)有一個(gè)slicecopy函數(shù),這個(gè)函數(shù)最終就是調(diào)用runtime.memmove來實(shí)現(xiàn)slice的copy的,我們看下源碼:

func slicecopy(to, fm slice, width uintptr) int {
    // 如果源切片或者目標(biāo)切片有一個(gè)長(zhǎng)度為0,那么就不需要拷貝,直接 return
    if fm.len == 0 || to.len == 0 {
        return 0
    }
     
    // n 記錄下源切片或者目標(biāo)切片較短的那一個(gè)的長(zhǎng)度
    n := fm.len
    if to.len < n {
        n = to.len
    }
 
    // 如果入?yún)?width = 0,也不需要拷貝了,返回較短的切片的長(zhǎng)度
    if width == 0 {
        return n
    }
 
    //如果開啟競(jìng)爭(zhēng)檢測(cè)
    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?
        //如果只有一個(gè)元素,那么直接進(jìn)行地址轉(zhuǎn)換
        *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
    } else {
        //如果不止一個(gè)元素,那么就從 fm.array 地址開始,拷貝到 to.array 地址之后,拷貝個(gè)數(shù)為size
        memmove(to.array, fm.array, size)
    }
    return n
}

源碼解讀見中文注釋。

6、值傳遞還是引用傳遞

slice在作為函數(shù)參數(shù)進(jìn)行傳遞的時(shí)候,是值傳遞還是引用傳遞,我們來看一段程序:

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

很簡(jiǎn)單的一段程序,我們直接來看輸出結(jié)果

[1] 1 10
[1] 1 10

可見fn內(nèi)的append操作并未對(duì)slice產(chǎn)生影響,那我們?cè)倏匆欢未a:

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居然改變了,是不是有點(diǎn)混亂?前面我們說到slice底層其實(shí)是一個(gè)結(jié)構(gòu)體,len、cap、array分別表示長(zhǎng)度、容量、底層數(shù)組的地址,當(dāng)slice作為函數(shù)的參數(shù)傳遞的時(shí)候,跟普通結(jié)構(gòu)體的傳遞是沒有區(qū)別的;如果直接傳slice,實(shí)參slice是不會(huì)被函數(shù)中的操作改變的,但是如果傳遞的是slice的指針,是會(huì)改變?cè)瓉淼膕lice的;另外,無論是傳遞slice還是slice的指針,如果改變了slice的底層數(shù)組,那么都是會(huì)影響slice的,這種通過數(shù)組下標(biāo)的方式更新slice數(shù)據(jù),是會(huì)對(duì)底層數(shù)組進(jìn)行改變的,所以就會(huì)影響slice。

那么,講到這里,在第一段程序中在fn函數(shù)內(nèi)append的5到哪里去了,不可能憑空消失啊,我們?cè)賮砜匆欢纬绦?/p>

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]//數(shù)組截取
    fmt.Println(s1, len(s1), cap(s1))
}
func fn(in []int) {
    in = append(in, 5)
}

我們來看輸出結(jié)果

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

顯然,雖然在append后,slice中并未展示出5,也無法通過slice[1]取到(會(huì)數(shù)組越界),但是實(shí)際上底層數(shù)組已經(jīng)有了5這個(gè)元素,但是由于slice的len未發(fā)生改變,所以我們?cè)谏蠈邮菬o法獲取到5這個(gè)元素的。那么,再問一個(gè)問題,我們是不是可以手動(dòng)強(qiáng)制改變slice的len長(zhǎng)度,讓我們可以獲取到5這個(gè)元素呢?是可以的,我們來看一段程序

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 //強(qiáng)制修改slice長(zhǎng)度
    fmt.Println(slice, len(slice), cap(slice))
}
 
func fn(in []int) {
    in = append(in, 5)
}

我們來看輸出結(jié)果

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

可以看出,通過強(qiáng)制修改slice的len,我們可以獲取到了5這個(gè)元素。

所以再次回答一開始我們提出的問題,slice是值傳遞還是引用傳遞?答案是值傳遞!

以上,在使用golang中的slice的時(shí)候大家一定注意,否則稍有不慎就會(huì)出現(xiàn)bug。

參考文獻(xiàn)

【1】《深入解析Go中Slice底層實(shí)現(xiàn)》:https://halfrost.com/go_slice/

【2】《理解Go中的Slice》:https://sanyuesha.com/2018/07/31/go-slice/

【3】《深度解密Go語言之Slice》:https://segmentfault.com/a/1190000019378931

【4】《The Go Programming Language Specification》:https://golang.org/ref/spec

到此這篇關(guān)于Golang 中 Slice的分析與使用(含源碼)的文章就介紹到這了,更多相關(guān)go slice使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 使用Go語言自制簡(jiǎn)單易用的Web框架

    使用Go語言自制簡(jiǎn)單易用的Web框架

    這篇文章主要為大家詳細(xì)介紹了如何使用Go語言實(shí)現(xiàn)自制簡(jiǎn)單易用的Web框架,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-01-01
  • 如何使用騰訊云go sdk 查詢對(duì)象存儲(chǔ)中最新文件

    如何使用騰訊云go sdk 查詢對(duì)象存儲(chǔ)中最新文件

    這篇文章主要介紹了使用騰訊云go sdk 查詢對(duì)象存儲(chǔ)中最新文件,這包括如何創(chuàng)建COS客戶端,如何逐頁檢索對(duì)象列表,并如何對(duì)結(jié)果排序以找到最后更新的對(duì)象,我們還展示了如何優(yōu)化用戶體驗(yàn),通過實(shí)時(shí)進(jìn)度更新和檢索多個(gè)文件來改進(jìn)程序,需要的朋友可以參考下
    2024-03-03
  • 快速升級(jí)Go版本(幾分鐘就搞定了)

    快速升級(jí)Go版本(幾分鐘就搞定了)

    go現(xiàn)在的更新速度是非常的快啊,用著用著網(wǎng)上的教程就不配套了,下面這篇文章主要給大家介紹了關(guān)于快速升級(jí)Go版本的相關(guān)資料,文中介紹的方法幾分鐘就搞定了,需要的朋友可以參考下
    2024-05-05
  • Go語言題解LeetCode888公平糖果交換示例詳解

    Go語言題解LeetCode888公平糖果交換示例詳解

    這篇文章主要為大家介紹了Go語言題解LeetCode888公平糖果交換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟

    go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟

    這篇文章主要為大家介紹了go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • Golang文件讀寫操作詳情

    Golang文件讀寫操作詳情

    這篇文章主要介紹了Golang文件讀寫操作詳情,文件是數(shù)據(jù)源(保存數(shù)據(jù)的地方)的一種,文件最主要的作用就是保存數(shù)據(jù),文件在程序中是以流的形式來操作的,更多詳細(xì)內(nèi)容需要的朋友可以參考一下
    2022-07-07
  • Go操作Kafka和Etcd方法詳解

    Go操作Kafka和Etcd方法詳解

    這篇文章主要為大家介紹了Go操作Kafka和Etcd方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Go 1.13中errors包的新變化示例解析

    Go 1.13中errors包的新變化示例解析

    這篇文章主要為大家介紹了Go 1.13中errors包的新變化示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Go微服務(wù)項(xiàng)目配置文件的定義和讀取示例詳解

    Go微服務(wù)項(xiàng)目配置文件的定義和讀取示例詳解

    這篇文章主要為大家介紹了Go微服務(wù)項(xiàng)目配置文件的定義和讀取示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • Go語言學(xué)習(xí)教程之反射的示例詳解

    Go語言學(xué)習(xí)教程之反射的示例詳解

    這篇文章主要通過記錄對(duì)reflect包的簡(jiǎn)單使用,來對(duì)反射有一定的了解。文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語言有一定幫助,需要的可以參考一下
    2022-09-09

最新評(píng)論