深入解析 Go 中 Slice 底層實(shí)現(xiàn)

切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu)黎比,使用這種結(jié)構(gòu)可以用來(lái)管理數(shù)據(jù)集合墨闲。切片的設(shè)計(jì)想法是由動(dòng)態(tài)數(shù)組概念而來(lái)氏涩,為了開(kāi)發(fā)者可以更加方便的使一個(gè)數(shù)據(jù)結(jié)構(gòu)可以自動(dòng)增加和減少酬诀。但是切片本身并不是動(dòng)態(tài)數(shù)據(jù)或者數(shù)組指針曹质。切片常見(jiàn)的操作有 reslice婴噩、append、copy羽德。與此同時(shí)几莽,切片還具有可索引,可迭代的優(yōu)秀特性宅静。

一. 切片和數(shù)組

關(guān)于切片和數(shù)組怎么選擇章蚣?接下來(lái)好好討論討論這個(gè)問(wèn)題。

在 Go 中姨夹,與 C 數(shù)組變量隱式作為指針使用不同纤垂,Go 數(shù)組是值類型矾策,賦值和函數(shù)傳參操作都會(huì)復(fù)制整個(gè)數(shù)組數(shù)據(jù)。


func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int

    arrayB = arrayA

    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)

    testArray(arrayA)
}

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}


打印結(jié)果:


arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]

可以看到峭沦,三個(gè)內(nèi)存地址都不同贾虽,這也就驗(yàn)證了 Go 中數(shù)組賦值和函數(shù)傳參都是值復(fù)制的。那這會(huì)導(dǎo)致什么問(wèn)題呢吼鱼?

假想每次傳參都用數(shù)組蓬豁,那么每次數(shù)組都要被復(fù)制一遍。如果數(shù)組大小有 100萬(wàn)菇肃,在64位機(jī)器上就需要花費(fèi)大約 800W 字節(jié)地粪,即 8MB 內(nèi)存。這樣會(huì)消耗掉大量的內(nèi)存巷送。于是乎有人想到驶忌,函數(shù)傳參用數(shù)組的指針矛辕。


func main() {
    arrayA := [2]int{100, 200}
    testArrayPoint(&arrayA)   // 1.傳數(shù)組指針
    arrayB := arrayA[:]
    testArrayPoint(&arrayB)   // 2.傳切片
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
}

func testArrayPoint(x *[]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
    (*x)[1] += 100
}

打印結(jié)果:


func Array : 0xc4200b0140 , [100 200]
func Array : 0xc4200b0180 , [100 300]
arrayA : 0xc4200b0140 , [100 400]

這也就證明了數(shù)組指針確實(shí)到達(dá)了我們想要的效果⌒︴耍現(xiàn)在就算是傳入10億的數(shù)組,也只需要再棧上分配一個(gè)8個(gè)字節(jié)的內(nèi)存給指針就可以了聊品。這樣更加高效的利用內(nèi)存飞蹂,性能也比之前的好。

不過(guò)傳指針會(huì)有一個(gè)弊端翻屈,從打印結(jié)果可以看到陈哑,第一行和第三行指針地址都是同一個(gè),萬(wàn)一原數(shù)組的指針指向更改了伸眶,那么函數(shù)里面的指針指向都會(huì)跟著更改惊窖。

切片的優(yōu)勢(shì)也就表現(xiàn)出來(lái)了。用切片傳數(shù)組參數(shù)厘贼,既可以達(dá)到節(jié)約內(nèi)存的目的界酒,也可以達(dá)到合理處理好共享內(nèi)存的問(wèn)題。打印結(jié)果第二行就是切片嘴秸,切片的指針和原來(lái)數(shù)組的指針是不同的毁欣。

由此我們可以得出結(jié)論:

把第一個(gè)大數(shù)組傳遞給函數(shù)會(huì)消耗很多內(nèi)存,采用切片的方式傳參可以避免上述問(wèn)題岳掐。切片是引用傳遞凭疮,所以它們不需要使用額外的內(nèi)存并且比使用數(shù)組更有效率。

但是串述,依舊有反例执解。


package main

import "testing"

func array() [1024]int {
    var x [1024]int
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}

func slice() []int {
    x := make([]int, 1024)
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}

func BenchmarkArray(b *testing.B) {
    for i := 0; i < b.N; i++ {
        array()
    }
}

func BenchmarkSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice()
    }
}

我們做一次性能測(cè)試,并且禁用內(nèi)聯(lián)和優(yōu)化纲酗,來(lái)觀察切片的堆上內(nèi)存分配的情況衰腌。


  go test -bench . -benchmem -gcflags "-N -l"

輸出結(jié)果比較“令人意外”:


BenchmarkArray-4          500000              3637 ns/op               0 B/op          0 alloc s/op
BenchmarkSlice-4          300000              4055 ns/op            8192 B/op          1 alloc s/op

解釋一下上述結(jié)果逝淹,在測(cè)試 Array 的時(shí)候,用的是4核桶唐,循環(huán)次數(shù)是500000栅葡,平均每次執(zhí)行時(shí)間是3637 ns,每次執(zhí)行堆上分配內(nèi)存總量是0尤泽,分配次數(shù)也是0 欣簇。

而切片的結(jié)果就“差”一點(diǎn),同樣也是用的是4核坯约,循環(huán)次數(shù)是300000熊咽,平均每次執(zhí)行時(shí)間是4055 ns,但是每次執(zhí)行一次闹丐,堆上分配內(nèi)存總量是8192横殴,分配次數(shù)也是1 。

這樣對(duì)比看來(lái)卿拴,并非所有時(shí)候都適合用切片代替數(shù)組衫仑,因?yàn)榍衅讓訑?shù)組可能會(huì)在堆上分配內(nèi)存,而且小數(shù)組在棧上拷貝的消耗也未必比
make 消耗大堕花。

二. 切片的數(shù)據(jù)結(jié)構(gòu)

切片本身并不是動(dòng)態(tài)數(shù)組或者數(shù)組指針文狱。它內(nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)通過(guò)指針引用底層數(shù)組,設(shè)定相關(guān)屬性將數(shù)據(jù)讀寫(xiě)操作限定在指定的區(qū)域內(nèi)缘挽。切片本身是一個(gè)只讀對(duì)象瞄崇,其工作機(jī)制類似數(shù)組指針的一種封裝

切片(slice)是對(duì)數(shù)組一個(gè)連續(xù)片段的引用壕曼,所以切片是一個(gè)引用類型(因此更類似于 C/C++ 中的數(shù)組類型苏研,或者 Python 中的 list 類型)。這個(gè)片段可以是整個(gè)數(shù)組腮郊,或者是由起始和終止索引標(biāo)識(shí)的一些項(xiàng)的子集摹蘑。需要注意的是,終止索引標(biāo)識(shí)的項(xiàng)不包括在切片內(nèi)伴榔。切片提供了一個(gè)與指向數(shù)組的動(dòng)態(tài)窗口纹蝴。

給定項(xiàng)的切片索引可能比相關(guān)數(shù)組的相同元素的索引小。和數(shù)組不同的是踪少,切片的長(zhǎng)度可以在運(yùn)行時(shí)修改塘安,最小為 0 最大為相關(guān)數(shù)組的長(zhǎng)度:切片是一個(gè)長(zhǎng)度可變的數(shù)組。

Slice 的數(shù)據(jù)結(jié)構(gòu)定義如下:



type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片的結(jié)構(gòu)體由3部分構(gòu)成援奢,Pointer 是指向一個(gè)數(shù)組的指針兼犯,len 代表當(dāng)前切片的長(zhǎng)度,cap 是當(dāng)前切片的容量。cap 總是大于等于 len 的切黔。

如果想從 slice 中得到一塊內(nèi)存地址砸脊,可以這樣做:


s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])

如果反過(guò)來(lái)呢?從 Go 的內(nèi)存地址中構(gòu)造一個(gè) slice纬霞。



var ptr unsafe.Pointer
var s1 = struct {
    addr uintptr
    len int
    cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))

構(gòu)造一個(gè)虛擬的結(jié)構(gòu)體凌埂,把 slice 的數(shù)據(jù)結(jié)構(gòu)拼出來(lái)。

當(dāng)然還有更加直接的方法诗芜,在 Go 的反射中就存在一個(gè)與之對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu) SliceHeader瞳抓,我們可以用它來(lái)構(gòu)造一個(gè) slice


var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

三. 創(chuàng)建切片

make 函數(shù)允許在運(yùn)行期動(dòng)態(tài)指定數(shù)組長(zhǎng)度,繞開(kāi)了數(shù)組類型必須使用編譯期常量的限制伏恐。

創(chuàng)建切片有兩種形式孩哑,make 創(chuàng)建切片,空切片翠桦。

1. make 和切片字面量


func makeslice(et *_type, len, cap int) slice {
    // 根據(jù)切片的數(shù)據(jù)類型横蜒,獲取切片的最大容量
    maxElements := maxSliceCap(et.size)
    // 比較切片的長(zhǎng)度憾朴,長(zhǎng)度值域應(yīng)該在[0,maxElements]之間
    if len < 0 || uintptr(len) > maxElements {
        panic(errorString("makeslice: len out of range"))
    }
    // 比較切片的容量申屹,容量值域應(yīng)該在[len,maxElements]之間
    if cap < len || uintptr(cap) > maxElements {
        panic(errorString("makeslice: cap out of range"))
    }
    // 根據(jù)切片的容量申請(qǐng)內(nèi)存
    p := mallocgc(et.size*uintptr(cap), et, true)
    // 返回申請(qǐng)好內(nèi)存的切片的首地址
    return slice{p, len, cap}
}

還有一個(gè) int64 的版本:


func makeslice64(et *_type, len64, cap64 int64) slice {
    len := int(len64)
    if int64(len) != len64 {
        panic(errorString("makeslice: len out of range"))
    }

    cap := int(cap64)
    if int64(cap) != cap64 {
        panic(errorString("makeslice: cap out of range"))
    }

    return makeslice(et, len, cap)
}

實(shí)現(xiàn)原理和上面的是一樣的,只不過(guò)多了把 int64 轉(zhuǎn)換成 int 這一步罷了蒙兰。

上圖是用 make 函數(shù)創(chuàng)建的一個(gè) len = 4闻鉴, cap = 6 的切片茵乱。內(nèi)存空間申請(qǐng)了6個(gè) int 類型的內(nèi)存大小。由于 len = 4孟岛,所以后面2個(gè)暫時(shí)訪問(wèn)不到,但是容量還是在的督勺。這時(shí)候數(shù)組里面每個(gè)變量都是0 渠羞。

除了 make 函數(shù)可以創(chuàng)建切片以外,字面量也可以創(chuàng)建切片智哀。

這里是用字面量創(chuàng)建的一個(gè) len = 6次询,cap = 6 的切片,這時(shí)候數(shù)組里面每個(gè)元素的值都初始化完成了瓷叫。需要注意的是 [ ] 里面不要寫(xiě)數(shù)組的容量屯吊,因?yàn)槿绻麑?xiě)了個(gè)數(shù)以后就是數(shù)組了,而不是切片了摹菠。

還有一種簡(jiǎn)單的字面量創(chuàng)建切片的方法盒卸。如上圖。上圖就 Slice A 創(chuàng)建出了一個(gè) len = 3次氨,cap = 3 的切片蔽介。從原數(shù)組的第二位元素(0是第一位)開(kāi)始切,一直切到第四位為止(不包括第五位)。同理虹蓄,Slice B 創(chuàng)建出了一個(gè) len = 2犀呼,cap = 4 的切片。

2. nil 和空切片

nil 切片和空切片也是常用的薇组。


var slice []int

nil 切片被用在很多標(biāo)準(zhǔn)庫(kù)和內(nèi)置函數(shù)中外臂,描述一個(gè)不存在的切片的時(shí)候,就需要用到 nil 切片律胀。比如函數(shù)在發(fā)生異常的時(shí)候专钉,返回的切片就是 nil 切片。nil 切片的指針指向 nil累铅。

空切片一般會(huì)用來(lái)表示一個(gè)空的集合跃须。比如數(shù)據(jù)庫(kù)查詢,一條結(jié)果也沒(méi)有查到娃兽,那么就可以返回一個(gè)空切片菇民。


silce := make( []int , 0 )
slice := []int{ }

空切片和 nil 切片的區(qū)別在于,空切片指向的地址不是nil投储,指向的是一個(gè)內(nèi)存地址第练,但是它沒(méi)有分配任何內(nèi)存空間,即底層元素包含0個(gè)元素玛荞。

最后需要說(shuō)明的一點(diǎn)是娇掏。不管是使用 nil 切片還是空切片,對(duì)其調(diào)用內(nèi)置函數(shù) append勋眯,len 和 cap 的效果都是一樣的婴梧。

四. 切片擴(kuò)容

當(dāng)一個(gè)切片的容量滿了,就需要擴(kuò)容了客蹋。怎么擴(kuò)塞蹭,策略是什么?


func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要擴(kuò)容的容量比原來(lái)的容量還要小讶坯,這代表要縮容了番电,那么可以直接報(bào)panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果當(dāng)前切片的大小為0辆琅,還調(diào)用了擴(kuò)容方法漱办,那么就新生成一個(gè)新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

  // 這里就是擴(kuò)容的策略
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    // 計(jì)算新的切片的容量婉烟,長(zhǎng)度娩井。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判斷非法的值,保證容量是在增加隅很,并且容量不超過(guò)最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        // 在老的切片后面繼續(xù)擴(kuò)充容量
        p = mallocgc(capmem, nil, false)
        // 將 lenmem 這個(gè)多個(gè) bytes 從 old.array地址 拷貝到 p 的地址處
        memmove(p, old.array, lenmem)
        // 先將 P 地址加上新的容量得到新切片容量的地址撞牢,然后將新切片容量地址后面的 capmem-newlenmem 個(gè) bytes 這塊內(nèi)存初始化率碾。為之后繼續(xù) append() 操作騰出空間。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申請(qǐng)新的數(shù)組給新切片
        // 重新申請(qǐng) capmen 這個(gè)大的內(nèi)存地址屋彪,并且初始化為0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果還不能打開(kāi)寫(xiě)鎖所宰,那么只能把 lenmem 大小的 bytes 字節(jié)從 old.array 拷貝到 p 的地址處
            memmove(p, old.array, lenmem)
        } else {
            // 循環(huán)拷貝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最終新切片,容量更新為最新擴(kuò)容之后的容量
    return slice{p, old.len, newcap}
}

上述就是擴(kuò)容的實(shí)現(xiàn)畜挥。主要需要關(guān)注的有兩點(diǎn)仔粥,一個(gè)是擴(kuò)容時(shí)候的策略,還有一個(gè)就是擴(kuò)容是生成全新的內(nèi)存地址還是在原來(lái)的地址后追加蟹但。

1. 擴(kuò)容策略

先看看擴(kuò)容策略躯泰。


func main() {
    slice := []int{10, 20, 30, 40}
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}

輸出結(jié)果:


Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8

用圖表示出上述過(guò)程。

從圖上我們可以很容易的看出华糖,新的切片和之前的切片已經(jīng)不同了麦向,因?yàn)樾碌那衅牧艘粋€(gè)值,并沒(méi)有影響到原來(lái)的數(shù)組客叉,新切片指向的數(shù)組是一個(gè)全新的數(shù)組诵竭。并且 cap 容量也發(fā)生了變化。這之間究竟發(fā)生了什么呢兼搏?

Go 中切片擴(kuò)容的策略是這樣的:

如果切片的容量小于 1024 個(gè)元素卵慰,于是擴(kuò)容的時(shí)候就翻倍增加容量。上面那個(gè)例子也驗(yàn)證了這一情況佛呻,總?cè)萘繌脑瓉?lái)的4個(gè)翻倍到現(xiàn)在的8個(gè)裳朋。

一旦元素個(gè)數(shù)超過(guò) 1024 個(gè)元素,那么增長(zhǎng)因子就變成 1.25 吓著,即每次增加原來(lái)容量的四分之一鲤嫡。

注意:擴(kuò)容擴(kuò)大的容量都是針對(duì)原來(lái)的容量而言的,而不是針對(duì)原來(lái)數(shù)組的長(zhǎng)度而言的夜矗。

2. 新數(shù)組 or 老數(shù)組 泛范?

再談?wù)剶U(kuò)容之后的數(shù)組一定是新的么?這個(gè)不一定紊撕,分兩種情況。

情況一:


func main() {
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    fmt.Printf("After array = %v\n", array)
}

打印輸出:


Before slice = [10 20], Pointer = 0xc4200c0040, len = 2, cap = 4
Before newSlice = [10 20 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After slice = [10 30], Pointer = 0xc4200c0040, len = 2, cap = 4
After newSlice = [10 30 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After array = [10 30 50 40]

把上述過(guò)程用圖表示出來(lái)赡突,如下圖对扶。

通過(guò)打印的結(jié)果,我們可以看到惭缰,在這種情況下浪南,擴(kuò)容以后并沒(méi)有新建一個(gè)新的數(shù)組,擴(kuò)容前后的數(shù)組都是同一個(gè)漱受,這也就導(dǎo)致了新的切片修改了一個(gè)值络凿,也影響到了老的切片了。并且 append() 操作也改變了原來(lái)數(shù)組里面的值。一個(gè) append() 操作影響了這么多地方絮记,如果原數(shù)組上有多個(gè)切片摔踱,那么這些切片都會(huì)被影響!無(wú)意間就產(chǎn)生了莫名的 bug怨愤!

這種情況派敷,由于原數(shù)組還有容量可以擴(kuò)容,所以執(zhí)行 append() 操作以后撰洗,會(huì)在原數(shù)組上直接操作篮愉,所以這種情況下,擴(kuò)容以后的數(shù)組還是指向原來(lái)的數(shù)組差导。

這種情況也極容易出現(xiàn)在字面量創(chuàng)建切片時(shí)候试躏,第三個(gè)參數(shù) cap 傳值的時(shí)候,如果用字面量創(chuàng)建切片设褐,cap 并不等于指向數(shù)組的總?cè)萘康咴蹋敲催@種情況就會(huì)發(fā)生。


slice := array[1:2:3]

上面這種情況非常危險(xiǎn)络断,極度容易產(chǎn)生 bug 裁替。

建議用字面量創(chuàng)建切片的時(shí)候,cap 的值一定要保持清醒貌笨,避免共享原數(shù)組導(dǎo)致的 bug弱判。

情況二:

情況二其實(shí)就是在擴(kuò)容策略里面舉的例子,在那個(gè)例子中之所以生成了新的切片锥惋,是因?yàn)樵瓉?lái)數(shù)組的容量已經(jīng)達(dá)到了最大值昌腰,再想擴(kuò)容, Go 默認(rèn)會(huì)先開(kāi)一片內(nèi)存區(qū)域膀跌,把原來(lái)的值拷貝過(guò)來(lái)遭商,然后再執(zhí)行 append() 操作。這種情況絲毫不影響原數(shù)組捅伤。

所以建議盡量避免情況一劫流,盡量使用情況二,避免 bug 產(chǎn)生丛忆。

五. 切片拷貝

Slice 中拷貝方法有2個(gè)祠汇。


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
    }
    // 如果開(kāi)啟了競(jìng)爭(zhēng)檢測(cè)
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&to))
        pc := funcPC(slicecopy)
        racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
        racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
    }
    // 如果開(kāi)啟了 The memory sanitizer (msan)
    if msanenabled {
        msanwrite(to.array, uintptr(n*int(width)))
        msanread(fm.array, uintptr(n*int(width)))
    }

    size := uintptr(n) * width
    if size == 1 { 
        // TODO: is this still worth it with new memmove impl?
        // 如果只有一個(gè)元素凰浮,那么指針直接轉(zhuǎn)換即可
        *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
    } else {
        // 如果不止一個(gè)元素我抠,那么就把 size 個(gè) bytes 從 fm.array 地址開(kāi)始苇本,拷貝到 to.array 地址之后
        memmove(to.array, fm.array, size)
    }
    return n
}


在這個(gè)方法中,slicecopy 方法會(huì)把源切片值(即 fm Slice )中的元素復(fù)制到目標(biāo)切片(即 to Slice )中菜拓,并返回被復(fù)制的元素個(gè)數(shù)瓣窄,copy 的兩個(gè)類型必須一致。slicecopy 方法最終的復(fù)制結(jié)果取決于較短的那個(gè)切片尘惧,當(dāng)較短的切片復(fù)制完成康栈,整個(gè)復(fù)制過(guò)程就全部完成了。

舉個(gè)例子喷橙,比如:


func main() {
    array := []int{10, 20, 30, 40}
    slice := make([]int, 6)
    n := copy(slice, array)
    fmt.Println(n,slice)
}

還有一個(gè)拷貝的方法啥么,這個(gè)方法原理和 slicecopy 方法類似,不在贅述了贰逾,注釋寫(xiě)在代碼里面了悬荣。



func slicestringcopy(to []byte, fm string) int {
    // 如果源切片或者目標(biāo)切片有一個(gè)長(zhǎng)度為0,那么就不需要拷貝疙剑,直接 return 
    if len(fm) == 0 || len(to) == 0 {
        return 0
    }
    // n 記錄下源切片或者目標(biāo)切片較短的那一個(gè)的長(zhǎng)度
    n := len(fm)
    if len(to) < n {
        n = len(to)
    }
    // 如果開(kāi)啟了競(jìng)爭(zhēng)檢測(cè)
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&to))
        pc := funcPC(slicestringcopy)
        racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
    }
    // 如果開(kāi)啟了 The memory sanitizer (msan)
    if msanenabled {
        msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
    }
    // 拷貝字符串至字節(jié)數(shù)組
    memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
    return n
}


再舉個(gè)例子氯迂,比如:


func main() {
    slice := make([]byte, 3)
    n := copy(slice, "abcdef")
    fmt.Println(n,slice)
}

輸出:


3 [97,98,99]

說(shuō)到拷貝,切片中有一個(gè)需要注意的問(wèn)題言缤。


func main() {
    slice := []int{10, 20, 30, 40}
    for index, value := range slice {
        fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    }
}

輸出:


value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338


從上面結(jié)果我們可以看到嚼蚀,如果用 range 的方式去遍歷一個(gè)切片,拿到的 Value 其實(shí)是切片里面的值拷貝管挟。所以每次打印 Value 的地址都不變轿曙。

由于 Value 是值拷貝的,并非引用傳遞僻孝,所以直接改 Value 是達(dá)不到更改原切片值的目的的导帝,需要通過(guò) &slice[index] 獲取真實(shí)的地址。


Reference:
《Go in action》
《Go 語(yǔ)言學(xué)習(xí)筆記》

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: https://halfrost.com/go_slice/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末穿铆,一起剝皮案震驚了整個(gè)濱河市您单,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荞雏,老刑警劉巖虐秦,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異凤优,居然都是意外死亡羡疗,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)别洪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人柳刮,你說(shuō)我怎么就攤上這事挖垛⊙鞫郏” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵痢毒,是天一觀的道長(zhǎng)送矩。 經(jīng)常有香客問(wèn)我,道長(zhǎng)哪替,這世上最難降的妖魔是什么栋荸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮凭舶,結(jié)果婚禮上晌块,老公的妹妹穿的比我還像新娘。我一直安慰自己帅霜,他們只是感情好匆背,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著身冀,像睡著了一般钝尸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搂根,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天珍促,我揣著相機(jī)與錄音,去河邊找鬼剩愧。 笑死猪叙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的隙咸。 我是一名探鬼主播沐悦,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼五督!你這毒婦竟也來(lái)了藏否?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤充包,失蹤者是張志新(化名)和其女友劉穎副签,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體基矮,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淆储,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了家浇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片本砰。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钢悲,靈堂內(nèi)的尸體忽然破棺而出点额,到底是詐尸還是另有隱情舔株,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布还棱,位于F島的核電站载慈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏珍手。R本人自食惡果不足惜办铡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琳要。 院中可真熱鬧寡具,春花似錦、人聲如沸焙蹭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)孔厉。三九已至拯钻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撰豺,已是汗流浹背粪般。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留污桦,地道東北人亩歹。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像凡橱,于是被迫代替她去往敵國(guó)和親小作。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359