golang筆記——深入了解Slice底層原理

大家可以看下面這道關(guān)于slice的題目,通過(guò)這道題我們可以對(duì)slice的特性和注意事項(xiàng)有一個(gè)深入理解邪驮。

package main

import "fmt"

func main() {
    a := [...]int{0, 1, 2, 3}
    x := a[:1]
    y := a[2:]
    x = append(x, y...)
    x = append(x, y...)
    fmt.Println(a, x)
}
  • A. [0 1 2 3] [0 2 3 3 3]
  • B. [0 2 3 3] [0 2 3 3 3]
  • C. [0 1 2 3] [0 2 3 2 3]
  • D. [0 2 3 3] [0 2 3 2 3]
    這道題有幾個(gè)考點(diǎn):
  1. slice的底層數(shù)據(jù)結(jié)構(gòu)是什么谁帕?給slice賦值辩蛋,到底賦了什么內(nèi)容技健?
  2. 通過(guò):操作得到的新slice和原slice是什么關(guān)系骄呼?新slice的長(zhǎng)度和容量是多少?
  3. append在背后到底做了哪些事情琼富?
  4. slice的擴(kuò)容機(jī)制是什么仪吧?

解析


Slice的底層數(shù)據(jù)結(jié)構(gòu)

slice定義在src/runtime/slice.go第15行,源碼地址:https://github.com/golang/go/blob/master/src/runtime/slice.go#L15鞠眉。

Pointer定義在src/unsafe/unsafe.go第184行邑商,源碼地址:https://github.com/golang/go/blob/master/src/unsafe/unsafe.go#L184。

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

type Pointer *ArbitraryType

slice實(shí)際上是一個(gè)結(jié)構(gòu)體類(lèi)型凡蚜,包含3個(gè)字段人断,分別是

  • array: 是指針,指向一個(gè)數(shù)組朝蜘,切片的數(shù)據(jù)實(shí)際都存儲(chǔ)在這個(gè)數(shù)組里恶迈。
  • len: 切片的長(zhǎng)度。
  • cap: 切片的容量谱醇,表示切片當(dāng)前最多可以存儲(chǔ)多少個(gè)元素暇仲,如果超過(guò)了現(xiàn)有容量會(huì)自動(dòng)擴(kuò)容。

因此給slice賦值副渴,實(shí)際上都是給slice里的這3個(gè)字段賦值奈附。看起來(lái)這像是一句正確的廢話(huà)煮剧,但是相信我斥滤,記住這句話(huà)可以幫助你非常清晰地理解對(duì)slice做修改后slice里3個(gè)字段的值是怎么變的,slice 指向的底層數(shù)組的數(shù)據(jù)是怎么變的勉盅。

:分割操作符

:分割操作符有幾個(gè)特點(diǎn):

  1. :可以對(duì)數(shù)組或者slice做數(shù)據(jù)截取佑颇,:得到的結(jié)果是一個(gè)新slice

  2. slice結(jié)構(gòu)體里的array指針指向原數(shù)組或者原slice的底層數(shù)組草娜,新切片的長(zhǎng)度是右邊的數(shù)值減去左邊的數(shù)值挑胸,新切片的容量是原切片的容量減去:左邊的數(shù)值。

  3. :的左邊如果沒(méi)有寫(xiě)數(shù)字宰闰,左邊的默認(rèn)值是0茬贵,右邊如果沒(méi)有寫(xiě)數(shù)字,右邊的默認(rèn)值是被分割的數(shù)組或被分割的切片的長(zhǎng)度移袍,注意解藻,是長(zhǎng)度不是容量。

    a := make([]int, 0, 4) // a的長(zhǎng)度是0咐容,容量是4
    b := a[:] // 等價(jià)于 b := a[0:0], b的長(zhǎng)度是0舆逃,容量是4
    c := a[:1] // 等價(jià)于 c := a[0:1], c的長(zhǎng)度是1蚂维,容量是4
    d := a[1:] // 編譯報(bào)錯(cuò) panic: runtime error: slice bounds out of range
    e := a[1:4] // e的長(zhǎng)度3戳粒,容量3
    
  4. :分割操作符右邊的數(shù)值有上限路狮,上限有2種情況

  • 如果分割的是數(shù)組,那上限是是被分割的數(shù)組的長(zhǎng)度蔚约。
  • 如果分割的是切片奄妨,那上限是被分割的切片的容量。注意苹祟,這個(gè)和下標(biāo)操作不一樣砸抛,如果使用下標(biāo)索引訪(fǎng)問(wèn)切片,下標(biāo)索引的最大值是(切片的長(zhǎng)度-1)树枫,而不是切片的容量直焙。

一圖勝千言,我們通過(guò)下面的示例來(lái)講解下切片分割的機(jī)制砂轻。

下圖表示slice結(jié)構(gòu)奔誓,ptr表示array指針,指向底層數(shù)組搔涝,lencap分別是切片的長(zhǎng)度和容量厨喂。

step1: 我們通過(guò)代碼s := make([]byte, 5, 5)來(lái)創(chuàng)建一個(gè)切片s,長(zhǎng)度和容量都是5庄呈,結(jié)構(gòu)示意如下:

step2: 現(xiàn)在對(duì)切片s做分割s2 := s[2:4]蜕煌,得到一個(gè)新切片s2,結(jié)構(gòu)如下诬留。

  • s2還是指向原切片s的底層數(shù)組斜纪,只不過(guò)指向的起始位置是下標(biāo)索引為2的位置。
  • s2的長(zhǎng)度len(s2)是2文兑,因?yàn)?code>s2 := s[2:4]只是截取了切片s下標(biāo)索引為2和3的2個(gè)元素傀广。
  • s2的容量cap(s2)是3,因?yàn)閺?code>s2指向的數(shù)組位置到底層數(shù)組末尾彩届,可以存3個(gè)元素伪冰。
  • 因?yàn)殚L(zhǎng)度是2,所以只有s2[0]s2[1]是有效的下標(biāo)索引訪(fǎng)問(wèn)樟蠕。但是贮聂,容量為3,s2[0:3]是一個(gè)有效的分割表達(dá)式寨辩。

step3: 對(duì)切片s做分割s3 := s2[:cap(s2)]吓懈,得到一個(gè)新切片s3,結(jié)構(gòu)如下:

  • s3指向切片s2的底層數(shù)組靡狞,同樣也是s的底層數(shù)組耻警,指向的起始位置是s2的起始位置,對(duì)應(yīng)數(shù)組下標(biāo)索引為2的位置。
  • s3的長(zhǎng)度len(s3)是3甘穿,因?yàn)?code>s3 := s2[:cap(s2)]截取了切片s2 下標(biāo)索引為0腮恩,1,2的3個(gè)元素温兼。
  • s3的容量cap(s3)是3秸滴,因?yàn)閺?code>s3指向的數(shù)組位置到底層數(shù)組末尾,可以存3個(gè)元素募判。

因此荡含,對(duì)數(shù)組或者切片做:分割操作產(chǎn)生的新切片還是指向原來(lái)的底層數(shù)組,并不會(huì)把原底層數(shù)組的元素拷貝一份到新的內(nèi)存空間里届垫。

正是因?yàn)樗麄冎赶蛲粔K內(nèi)存空間释液,所以對(duì)原數(shù)組或者原切片的修改會(huì)影響分割后的新切片的值,反之亦然装处。

append機(jī)制

要了解append的機(jī)制均澳,直接看源碼說(shuō)明。

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//  slice = append(slice, elem1, elem2)
//  slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//  slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
  • append函數(shù)返回的是一個(gè)切片符衔,append在原切片的末尾添加新元素找前,這個(gè)末尾是切片長(zhǎng)度的末尾,不是切片容量的末尾判族。

    func test() {
      a := make([]int, 0, 4)
      b := append(a, 1) // b=[1], a指向的底層數(shù)組的首元素為1躺盛,但是a的長(zhǎng)度和容量不變
      c := append(a, 2) // a的長(zhǎng)度還是0,c=[2], a指向的底層數(shù)組的首元素變?yōu)?
      fmt.Println(a, b, c) // [] [2] [2]
    }
    
  • 如果原切片的容量足以包含新增加的元素形帮,那append函數(shù)返回的切片結(jié)構(gòu)里3個(gè)字段的值是:

    • array指針字段的值不變槽惫,和原切片的array指針的值相同,也就是append是在原切片的底層數(shù)組添加元素辩撑,返回的切片還是指向原切片的底層數(shù)組
    • len長(zhǎng)度字段的值做相應(yīng)增加界斜,增加了N個(gè)元素,長(zhǎng)度就增加N
    • cap容量不變
  • 如果原切片的容量不夠存儲(chǔ)append新增加的元素合冀,Go會(huì)先分配一塊容量更大的新內(nèi)存各薇,然后把原切片里的所有元素拷貝過(guò)來(lái),最后在新的內(nèi)存里添加新元素君躺。append函數(shù)返回的切片結(jié)構(gòu)里的3個(gè)字段的值是:

    • array指針字段的值變了峭判,不再指向原切片的底層數(shù)組了,會(huì)指向一塊新的內(nèi)存空間
    • len長(zhǎng)度字段的值做相應(yīng)增加棕叫,增加了N個(gè)元素林螃,長(zhǎng)度就增加N
    • cap容量會(huì)增加

注意:append不會(huì)改變?cè)衅闹担衅拈L(zhǎng)度和容量都不變俺泣,除非把a(bǔ)ppend的返回值賦值給原切片疗认。

那么問(wèn)題來(lái)了完残,新切片的容量是按照什么規(guī)則計(jì)算得出來(lái)的呢?

slice擴(kuò)容機(jī)制

slice的擴(kuò)容機(jī)制隨著Go的版本迭代横漏,是有變化的谨设。目前網(wǎng)上大部分的說(shuō)法是下面這個(gè):

當(dāng)原 slice 容量小于 1024 的時(shí)候,新 slice 容量變成原來(lái)的 2 倍绊茧;原 slice 容量超過(guò) 1024铝宵,新 slice 容量變成原來(lái)的1.25倍打掘。
這里明確告訴大家华畏,這個(gè)結(jié)論是錯(cuò)誤的。

Go 1.18的slice擴(kuò)容機(jī)制為:

  1. 當(dāng)申請(qǐng)的容量(原 slice 容量+新元素個(gè)數(shù))大于2倍原slice容量的時(shí)候,新slice 容量變成申請(qǐng)的容量;
  2. 否則原 slice 容量小于 256的時(shí)候虑绵, 新 slice 容量變成原來(lái)的 2 倍谬盐;如果原 slice 容量>= 256,則循環(huán)執(zhí)行newcap += (newcap + 3*threshold) / 4绅喉,直到newcap>cap
  3. 如果newcap過(guò)大越界,則新slice 容量變成申請(qǐng)的容量。

Go 1.18的擴(kuò)容實(shí)現(xiàn)代碼如下晰甚,growslice的參數(shù)et是切片里的元素類(lèi)型,old是原切片决帖,cap等于原切片的長(zhǎng)度+append新增的元素個(gè)數(shù)厕九。(注意第3個(gè)參數(shù)cap的值是原切片的長(zhǎng)度+append新增元素個(gè)數(shù),不是原切片容量+新增元素個(gè)數(shù)地回,可以在growslice里打印cap的值來(lái)驗(yàn)證)

// src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ...
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                // Transition from growing 2x for small slices
                // to growing 1.25x for large slices. This formula
                // gives a smooth-ish transition between the two.
                newcap += (newcap + 3*threshold) / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    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 == goarch.PtrSize:
        lenmem = uintptr(old.len) * goarch.PtrSize
        newlenmem = uintptr(cap) * goarch.PtrSize
        capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
        newcap = int(capmem / goarch.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if goarch.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)
    }
    // ...
    return slice{p, old.len, newcap}
}

// src/runtime/msize.go:13
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize {
        if size <= smallSizeMax-8 {
            return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
        } else {
            return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
        }
    }
    if size+_PageSize < size {
        return size
    }
    return alignUp(size, _PageSize)
}

// src/runtime/sizeclass.go:84
const (
    _MaxSmallSize   = 32768
    smallSizeDiv    = 8
    smallSizeMax    = 1024
    largeSizeDiv    = 128
    _NumSizeClasses = 68
    _PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
var class_to_divmagic = [_NumSizeClasses]uint32{0, ^uint32(0)/8 + 1, ^uint32(0)/16 + 1, ^uint32(0)/24 + 1, ^uint32(0)/32 + 1, ^uint32(0)/48 + 1, ^uint32(0)/64 + 1, ^uint32(0)/80 + 1, ^uint32(0)/96 + 1, ^uint32(0)/112 + 1, ^uint32(0)/128 + 1, ^uint32(0)/144 + 1, ^uint32(0)/160 + 1, ^uint32(0)/176 + 1, ^uint32(0)/192 + 1, ^uint32(0)/208 + 1, ^uint32(0)/224 + 1, ^uint32(0)/240 + 1, ^uint32(0)/256 + 1, ^uint32(0)/288 + 1, ^uint32(0)/320 + 1, ^uint32(0)/352 + 1, ^uint32(0)/384 + 1, ^uint32(0)/416 + 1, ^uint32(0)/448 + 1, ^uint32(0)/480 + 1, ^uint32(0)/512 + 1, ^uint32(0)/576 + 1, ^uint32(0)/640 + 1, ^uint32(0)/704 + 1, ^uint32(0)/768 + 1, ^uint32(0)/896 + 1, ^uint32(0)/1024 + 1, ^uint32(0)/1152 + 1, ^uint32(0)/1280 + 1, ^uint32(0)/1408 + 1, ^uint32(0)/1536 + 1, ^uint32(0)/1792 + 1, ^uint32(0)/2048 + 1, ^uint32(0)/2304 + 1, ^uint32(0)/2688 + 1, ^uint32(0)/3072 + 1, ^uint32(0)/3200 + 1, ^uint32(0)/3456 + 1, ^uint32(0)/4096 + 1, ^uint32(0)/4864 + 1, ^uint32(0)/5376 + 1, ^uint32(0)/6144 + 1, ^uint32(0)/6528 + 1, ^uint32(0)/6784 + 1, ^uint32(0)/6912 + 1, ^uint32(0)/8192 + 1, ^uint32(0)/9472 + 1, ^uint32(0)/9728 + 1, ^uint32(0)/10240 + 1, ^uint32(0)/10880 + 1, ^uint32(0)/12288 + 1, ^uint32(0)/13568 + 1, ^uint32(0)/14336 + 1, ^uint32(0)/16384 + 1, ^uint32(0)/18432 + 1, ^uint32(0)/19072 + 1, ^uint32(0)/20480 + 1, ^uint32(0)/21760 + 1, ^uint32(0)/24576 + 1, ^uint32(0)/27264 + 1, ^uint32(0)/28672 + 1, ^uint32(0)/32768 + 1}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}

newcap是擴(kuò)容后的容量扁远,先根據(jù)原切片的長(zhǎng)度、容量和要添加的元素個(gè)數(shù)確定newcap大小刻像,最后再對(duì)newcap做內(nèi)存對(duì)齊得到最后的newcap畅买。

答案

我們回到本文最開(kāi)始的題目,逐行解析每行代碼的執(zhí)行結(jié)果细睡。

代碼 切片對(duì)應(yīng)結(jié)果
a := [...]int{0, 1, 2, 3} a是一個(gè)數(shù)組谷羞,長(zhǎng)度是4,值是[ 0 1 2 3]
x := a[:1] x是一個(gè)切片溜徙,切片里的指針指向數(shù)組a的首元素洒宝,值是[0],長(zhǎng)度1萌京,容量4
y := a[2:] y是一個(gè)切片雁歌,切片里的指針指向數(shù)組a的第2個(gè)元素,值是[2 3]知残,長(zhǎng)度2靠瞎,容量2
x = append(x, y...) x的剩余容量還有3個(gè)比庄,足以存儲(chǔ)y里的2個(gè)元素,所以x不會(huì)擴(kuò)容乏盐,x的值是[0 2 3]佳窑,長(zhǎng)度3,容量4父能。因?yàn)閤, a, y都指向同一塊內(nèi)存空間神凑,所以x的修改影響了a和y。
a的值變?yōu)閇0 2 3 3]何吝,長(zhǎng)度4溉委,容量4
y的值變?yōu)閇3 3],長(zhǎng)度2爱榕,容量2
x = append(x, y...) x的剩余容量只有1個(gè)瓣喊,不足以存儲(chǔ)y里的2個(gè)元素,所以要擴(kuò)容黔酥。append(x, y)的結(jié)果是得到一個(gè)新切片藻三,值是[0 2 3 3 3],長(zhǎng)度5跪者,容量8棵帽。
append的返回值賦值給x,所以切片x會(huì)指向擴(kuò)容后的新內(nèi)存渣玲。
fmt.Println(a, x) a的值還是[0 2 3 3]沒(méi)有變化逗概,所以打印結(jié)果是[0 2 3 3] [0 2 3 3 3 ],答案是B

加餐:copy機(jī)制

Go的內(nèi)置函數(shù)copy可以把一個(gè)切片里的元素拷貝到另一個(gè)切片柜蜈,源碼定義在src/builtin/builtin.go仗谆,代碼如下:

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int

copy會(huì)從原切片src拷貝 min(len(dst), len(src))個(gè)元素到目標(biāo)切片dst
因?yàn)榭截惖脑貍€(gè)數(shù)min(len(dst), len(src))不會(huì)超過(guò)目標(biāo)切片的長(zhǎng)度len(dst)淑履,所以copy執(zhí)行后隶垮,目標(biāo)切片的長(zhǎng)度不會(huì)變,容量不會(huì)變秘噪。
注意:原切片和目標(biāo)切片的內(nèi)存空間可能會(huì)有重合狸吞,copy后可能會(huì)改變?cè)衅闹担瑓⒖枷吕?/p>

package main
import "fmt"
func main() {
    a := []int{1, 2, 3}
    b := a[1:] // [2 3]
    copy(a, b) // a和b內(nèi)存空間有重疊
    fmt.Println(a, b) // [2 3 3] [3 3]
}

slice打印和底層數(shù)組地址

打印要弄清楚3個(gè)問(wèn)題:

  1. fmt.Println(slice)打印到切片底層數(shù)組的哪個(gè)元素截止指煎?

    根據(jù)切片的長(zhǎng)度len蹋偏,打印到下標(biāo)索引為len-1的元素截止。比如下例里至壤,雖然切片a的底層數(shù)組下標(biāo)索引len(a)-1后面還有個(gè)值1威始,但是因?yàn)閍的長(zhǎng)度為1,就只打印[0]像街,切片b的長(zhǎng)度為2黎棠,所以會(huì)打印[0 1]晋渺。

    a := make([]int, 1, 4) // a的長(zhǎng)度是1,容量是4
    b := append(a, 1) // 往a的末尾添加元素1脓斩,b=[0 1], a的長(zhǎng)度還是1木西,a和b指向同一個(gè)底層數(shù)組
    fmt.Println(a, b) // [0] [0 1]
    
  2. 如何打印slice結(jié)構(gòu)體變量的地址?

    s := []int{1, 2}
    fmt.Printf("%p\n", &s)
    
  3. 如何打印slice底層數(shù)組的地址随静?有2種方法

    s = make([]int, 2, 3)
    fmt.Printf("%p %p\n", s, &s[0])
    

總結(jié)

對(duì)于slice八千,時(shí)刻想著對(duì)slice做了修改后,slice里的3個(gè)字段:指針燎猛,長(zhǎng)度恋捆,容量是怎么變的。

  • slice是一個(gè)結(jié)構(gòu)體類(lèi)型扛门,里面包含3個(gè)字段:指向數(shù)組的array指針鸠信,長(zhǎng)度len和容量cap纵寝。給slice賦值是對(duì)slice里的指針论寨,長(zhǎng)度和容量3個(gè)字段分別賦值。

  • :分割操作符的結(jié)果是一個(gè)新切片爽茴,slice結(jié)構(gòu)體里的array指針指向原數(shù)組或者原slice的底層數(shù)組葬凳,新切片的長(zhǎng)度是右邊的數(shù)值減去左邊的數(shù)值,新切片的容量是原切片的容量減去:左邊的數(shù)值室奏。

  • :分割操作符右邊的數(shù)值上限有2種情況:

    • 如果分割的是數(shù)組火焰,那上限是是被分割的數(shù)組的長(zhǎng)度。
    • 如果分割的是切片胧沫,那上限是被分割的切片的容量昌简。注意,這個(gè)和下標(biāo)操作不一樣绒怨,如果使用下標(biāo)索引訪(fǎng)問(wèn)切片纯赎,下標(biāo)索引的最大值是(切片的長(zhǎng)度-1),而不是切片的容量南蹂。
  • 擴(kuò)容策略并不是簡(jiǎn)單的擴(kuò)為原切片容量的 2 倍或 1.25 倍犬金,還有內(nèi)存對(duì)齊的操作。擴(kuò)容后的容量 >= 原容量的 2 倍或 1.25 倍六剥。

  • 當(dāng)直接用切片作為函數(shù)參數(shù)時(shí)晚顷,可以改變切片的元素,不能改變切片本身疗疟;想要改變切片本身该默,可以將改變后的切片返回,函數(shù)調(diào)用者接收改變后的切片或者將切片指針作為函數(shù)參數(shù)策彤。

  • 打印slice時(shí)栓袖,是根據(jù)slice的長(zhǎng)度來(lái)打印的

    a := make([]int, 1, 4) // a的長(zhǎng)度是1顿膨,容量是4
    b := append(a, 1) // 往a的末尾添加元素1,b=[0 1], a的長(zhǎng)度還是1叽赊,a和b指向同一個(gè)底層數(shù)組
    fmt.Println(a, b) // [0] [0 1]
    fmt.Printf("%p %p\n", a, b) // 切片a和b的底層數(shù)組地址相同
    
  • Go在函數(shù)傳參時(shí)恋沃,沒(méi)有傳引用這個(gè)說(shuō)法,只有傳值必指。網(wǎng)上有些文章寫(xiě)Go的slice囊咏,mapchannel作為函數(shù)參數(shù)是傳引用塔橡,這是錯(cuò)誤的梅割,可以參考文章Go有引用變量和引用傳遞么?

思考題

留下2道思考題葛家,歡迎大家在評(píng)論區(qū)留下你們的答案户辞。

  • 題目1:

    package main
    
    import "fmt"
    
    func main() {
      a := []int{1, 2}
      b := append(a, 3)
    
      c := append(b, 4)
      d := append(b, 5)
    
      fmt.Println(a, b, c[3], d[3])
    }
    
  • 題目2

    package main
    
    import "fmt"
    
    func main() {
      s := []int{1, 2}
      s = append(s, 4, 5, 6)
      fmt.Println(len(s), cap(s))
    }
    

答案

  • 題目1:
代碼 切片對(duì)應(yīng)結(jié)果
a := []int{1, 2} a是一個(gè)數(shù)組,長(zhǎng)度是2癞谒,值是[ 1 2 ]
b := append(a, 3) a執(zhí)行append操作后底燎,a的底層數(shù)組將擴(kuò)容,擴(kuò)容時(shí)newcap < doublecap && newcap < 256弹砚,最終容量值為4双仍,返回新切片。b接收新切片桌吃,長(zhǎng)度是3朱沃,容量是4。 [ 1 2 3 ]
c := append(b, 4) b執(zhí)行append操作后茅诱,b的底層數(shù)組不會(huì)擴(kuò)容逗物。c為一個(gè)切片,長(zhǎng)度為4瑟俭, 容量為4翎卓,與b共享同一底層數(shù)組,底層數(shù)組值為[ 1 2 3 4 ]
d := append(b, 5) b執(zhí)行append操作后尔当,將從b的最后1位元素(3)之后追加值5莲祸。d為一個(gè)切片,長(zhǎng)度為4椭迎, 容量為4锐帜,與b共享同一底層數(shù)組,底層數(shù)組值為[ 1 2 3 5 ]
fmt.Println(a, b, c[3], d[3]) 底層數(shù)組最終值為[ 1 2 3 5 ]畜号,所以打印結(jié)果是[ 1 2 ] [ 1 2 3 ] 5 5

  • 題目2:
s := []int{1, 2}
s = append(s, 4, 5, 6)
 // 根據(jù)基礎(chǔ)的cap增長(zhǎng)規(guī)則我們可以計(jì)算出newcap為5缴阎,
// 但是計(jì)算出了新容量之后,出于內(nèi)存的高效利用考慮简软,還要進(jìn)行內(nèi)存對(duì)齊
// capmem := roundupsize(uintptr(newcap) * uintptr(et.size))
// newcap就是前文中計(jì)算出的newcap蛮拔,et.size代表slice中一個(gè)元素的大小述暂,
// capmem計(jì)算出來(lái)的就是此次擴(kuò)容需要申請(qǐng)的內(nèi)存大小。roundupsize函數(shù)就是處理內(nèi)存對(duì)齊的函數(shù)建炫。
// 根據(jù)源碼畦韭,我們最終會(huì)獲得uintptr(class_to_size[size_to_class8[(size+7)>>3]])為內(nèi)存大小(capmem = 48)
// 執(zhí)行內(nèi)存對(duì)齊后肛跌,最后通過(guò)newcap = int(capmem / goarch.PtrSize)獲得newcap = 6
fmt.Println(len(s), cap(s)) // 5 6

References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艺配,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子衍慎,更是在濱河造成了極大的恐慌转唉,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稳捆,死亡現(xiàn)場(chǎng)離奇詭異赠法,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)乔夯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)砖织,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人驯嘱,你說(shuō)我怎么就攤上這事镶苞≡梗” “怎么了鞠评?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)壕鹉。 經(jīng)常有香客問(wèn)我剃幌,道長(zhǎng),這世上最難降的妖魔是什么晾浴? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任负乡,我火速辦了婚禮,結(jié)果婚禮上脊凰,老公的妹妹穿的比我還像新娘抖棘。我一直安慰自己,他們只是感情好狸涌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布切省。 她就那樣靜靜地躺著,像睡著了一般帕胆。 火紅的嫁衣襯著肌膚如雪朝捆。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天懒豹,我揣著相機(jī)與錄音芙盘,去河邊找鬼驯用。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儒老,可吹牛的內(nèi)容都是我干的蝴乔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼驮樊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼淘这!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起巩剖,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铝穷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后佳魔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體曙聂,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年鞠鲜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宁脊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贤姆,死狀恐怖榆苞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情霞捡,我是刑警寧澤坐漏,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站碧信,受9級(jí)特大地震影響赊琳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砰碴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一躏筏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呈枉,春花似錦趁尼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至住册,卻和暖如春婶博,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工凡人, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留名党,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓挠轴,卻偏偏與公主長(zhǎng)得像传睹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岸晦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 前言 最近進(jìn)了煎魚(yú)大佬[https://www.bookstack.cn/read/eddycjy-go/READ...
    Xcxing閱讀 607評(píng)論 0 1
  • 參考鏈接: https://github.com/lvgithub/go_blog/blob/master/Boo...
    hsiaojun閱讀 607評(píng)論 0 0
  • 切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu)欧啤,使用這種結(jié)構(gòu)可以用來(lái)管理數(shù)據(jù)集合。切片的設(shè)計(jì)想法是由動(dòng)態(tài)數(shù)組概念而來(lái)启上,為了開(kāi)...
    一縷殤流化隱半邊冰霜閱讀 11,254評(píng)論 21 55
  • 數(shù)組是一種長(zhǎng)度不可改變的集合邢隧,go數(shù)組不僅如此,還是一個(gè)值變量冈在,也就是在賦值和函數(shù)調(diào)用采用值傳遞倒慧,會(huì)產(chǎn)生數(shù)組的全量...
    程序員阿土閱讀 877評(píng)論 0 2
  • 介紹 Go的切片類(lèi)型提供了一種方便有效的處理類(lèi)型數(shù)據(jù)序列的方法。切片類(lèi)似于其他語(yǔ)言中的數(shù)組包券,但具有一些不尋常的屬性...
    hewolf閱讀 527評(píng)論 0 0