Go 中 slice 的那些事

Go

一、定義

我們都知道在 Go 語言中,數(shù)組的長度是不可變的,那么為了更加靈活的處理數(shù)據(jù),Go 提供了一種功能強悍的類型切片(slice),slice 可以理解為 “動態(tài)數(shù)組”。但是 slice 并不是真正意義上的動態(tài)數(shù)組,而是一個引用類型。slice 總是指向一個底層 array,slice 的聲明也可以像 array 一樣,只是不需要長度。slice 的聲明和數(shù)組類似,如下

var iSlice []int

這里的聲明和數(shù)組一樣,只是少了長度,注意兩者的比較

//聲明一個保存 int 的 slice
var iSlice []int

//聲明一個長度為 10 的 int 數(shù)組
var iArray [10]int

還有一種聲明的方法是使用 make() 函數(shù),如下

slice1 := make([]int, 5, 10)

用 make() 函數(shù)創(chuàng)建的時候有三個參數(shù),make(type, len[, cap]) ,依次是類型、長度、容量。

slice1

如圖所示,上圖表示創(chuàng)建了 slice1 ,長度是 5,默認的值都是 0,容量是 10,這樣聲明就開辟了一塊容量是 10
的連續(xù)的一塊內(nèi)存。當然如果我們不指定容量也是可以的,如下

slice2 := make([]int, 5)

這樣就會根據(jù)實際情況動態(tài)分配內(nèi)存,而不是最開始指定一塊固定大小的內(nèi)存。需要注意的是我們一般使用 make() 函數(shù)來創(chuàng)建 slice,因為我們可以指定 slice 的容量,這樣在最開始創(chuàng)建的時候就分配好空間,避免數(shù)據(jù)多次改變導(dǎo)致多次重新改變 cap 分配空間帶來不必要的開銷。

二、slice 的特性

關(guān)于 slice 的一些基本特性,《Go Web 編程》 這本書里已經(jīng)講的很詳細,有對基本知識不清楚的童鞋可以去補習一下,這里就不一一敘述了。我么來看一個例子,

package main

import (
    "fmt"
)

func main() {
    aSlice := []int{1, 2, 3, 4, 5}
    fmt.Printf("aSlice length = %d, cap = %d, self = %v\n", len(aSlice), cap(aSlice), aSlice)
    aSlice = append(aSlice, 6)
    fmt.Printf("aSlice length= %d, cap = %d, self = %v", len(aSlice), cap(aSlice), aSlice)
}

這個時候我們運行,控制臺打印


我們會看到 aSlice 進行 append 操作以后,它的容量增加了一倍,cap 并沒有變成我們想象中的 6 ,而是變成了 10
aSlice

如果我們最開始 slice 的容量是 10,長度是 5 ,那么再加一個元素是不會改變切片的容量的。也就是說,當我們往 slice中增加元素超過原來的容量時,slice 會自增容量,當現(xiàn)有長度 < 1024 時 cap 增長是翻倍的,當超過 1024,cap 的增長是 1.25 倍增長。我們來看一下 slice.go 的源碼會發(fā)現(xiàn)有這樣一個函數(shù),里面說明了 cap 的增長規(guī)則

func growslice(et *_type, old slice, cap int) slice {
/**
    ....省略....
**/
    newcap := old.cap
    doublecap := newcap + newcap
    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
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
/**
    ....省略....
**/
}

從上面的源碼,在對 slice 進行 append 等操作時,可能會造成 slice 的自動擴容。其擴容時的大小增長規(guī)則是:

  • 如果新的 slice 大小是當前大小2倍以上,則大小增長為新大小
  • 否則循環(huán)以下操作:如果當前slice大小小于1024,按每次 2 倍增長,否則每次按當前大小 1/4 增長,直到增長的大小超過或等于新大小。
  • append 的實現(xiàn)只是簡單的在內(nèi)存中將舊 slice 復(fù)制給新 slice

來看一個例子,

package main

import "fmt"

func main() {

    aSlice := make([]int, 3, 5)
    bSlice := append(aSlice, 1, 2)
    fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
    fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
    aSlice[0] = 6
    fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
    fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))

}

我們會看到控制臺輸出

print

變化過程如下圖所示
slice變化

上面說明,在 slice 的 cap 范圍內(nèi)增加元素, slice 只會發(fā)生 len 的變化不會發(fā)生 cap 的變化,同樣也說明 slice 實際上是指向一個底層的數(shù)組,當多個 slice 指向同一個底層數(shù)組的時候,其中一個改變,其余的也會跟著改變,這里需要注意一下。我們同樣從 slice.go 的源碼中 slice 的定義可以看出,

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

這里關(guān)于底層的東西就不多敘述,有興趣的可以看看 一縷殤流化隱半邊冰霜 冰霜的 深入解析 Go 中 Slice 底層實現(xiàn) 這篇文章,對 slice 的底層實現(xiàn)的講解。接下來我們把上面的代碼改變一下

package main

import "fmt"

func main() {

    aSlice := make([]int, 3, 5)
    bSlice := append(aSlice, 1, 2, 3, 4)
    fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
    fmt.Printf("b %v , cap = %d, len = %d\n", bSlice, cap(bSlice), len(bSlice))
    aSlice[0] = 6
    fmt.Printf("a %v , cap = %d, len = %d\n", aSlice, cap(aSlice), len(aSlice))
    fmt.Printf("b %v , cap = %d, len = %d", bSlice, cap(bSlice), len(bSlice))

}

我們可以看到下面的輸出


改變后的 print

上面代碼可以用下圖說明


slice append

也就是說,當 append 的數(shù)據(jù)超過原來的容量以后,就會重新分配一塊新的內(nèi)存,并把原來的數(shù)據(jù) copy 過來,并且保留原來的空間,供原來的 slice(aSlice) 使用這樣 aSlice 和 bSlice 就各自指向不同的地址,當 aSlice 改變時,bSlice 不會改變。
關(guān)于 cap 還有一點需要注意,我們來用一個例子說明
package main

import "fmt"

func main() {

    Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
    Slice_a := Array_a[2:5]
    Slice_b := Slice_a[6:7]
    fmt.Printf("Slice_a %v , cap = %d, len = %d\n", string(Slice_a), cap(Slice_a), len(Slice_a))
    fmt.Printf("Slice_b %v , cap = %d, len = %d\n", string(Slice_b), cap(Slice_b), len(Slice_b))
}

控制臺打印


reslice print

這里我們會發(fā)現(xiàn) Slice_b 對 Slice_a 進行重新切片后,并沒有報錯,而是還有輸出,這是因為 Slice_a 的 cap 是 8 ,并不是我們想象的 3,slice 指向的是一塊連續(xù)的內(nèi)存,所以 Slice_a 的容量其實是一直到 Array_a 的最后的。所以這里 Array_b 對 Array_a 進行切片后會得到值,《Go Web 編程》 上這張圖形象的解釋了對數(shù)組的切片結(jié)果,這里是需要注意的一個點。

slice

三、關(guān)于 copy

我們來看下面代碼

package main

import "fmt"

func main() {

    aSlice := []int{1, 2, 3}
    bSlice := []int{4, 5, 6, 7, 8, 9}
    copy(bSlice, aSlice)
    fmt.Println(aSlice, bSlice)//[1 2 3] [1 2 3 7 8 9]
       //如果是 copy( aSlice, bSlice) 則結(jié)果是 [4 5 6] 
}

也就是說 copy() 函數(shù)有兩個參數(shù),一個是 to 一個是 from,就是將第二個 copy 到第一個上面,如果第一個長度小于第二個,那么就會 copy 與第一個等長度的值,如 copy( aSlice, bSlice) 的結(jié)果是 [4 5 6] ,反之則是短的覆蓋長的前幾位。當然我們也可以指定復(fù)制長度

package main

import "fmt"

func main() {

    aSlice := []int{1, 2, 3}
    bSlice := []int{4, 5, 6, 7, 8, 9}
    copy(bSlice[2:5], aSlice)
    fmt.Println(aSlice, bSlice)//[1 2 3] [4 5 1 2 3 9]
}

關(guān)于 slice 的 copy 的規(guī)則邏輯我們也可以在源碼中看出

func slicecopy(to, fm slice, width uintptr) int {
    if fm.len == 0 || to.len == 0 {
        return 0
    }

    n := fm.len
    if to.len < n {
        n = to.len
    }

    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 {
        memmove(to.array, fm.array, size)
    }
    return n
}

我們看源碼接著往下看會發(fā)現(xiàn)這樣一個方法

func slicestringcopy(to []byte, fm string) int {
    if len(fm) == 0 || len(to) == 0 {
        return 0
    }

    n := len(fm)
    if len(to) < n {
        n = len(to)
    }

    if raceenabled {
        callerpc := getcallerpc()
        pc := funcPC(slicestringcopy)
        racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
    }
    if msanenabled {
        msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
    }

    memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
    return n
}

我們會發(fā)現(xiàn)這個函數(shù)的兩個參數(shù)分別是 []byte 和 string ,這里其實是 Go 實現(xiàn)了一個將 string 復(fù)制到 []byte 上的方法,這個方法有什么用,我們來看個例子

package main

import "fmt"

func main() {

    s := "hello"
    c := []byte(s) // 將字符串 s 轉(zhuǎn)換為 []byte 類型
    c[0] = 'c'
    s2 := string(c) // 再轉(zhuǎn)換回 string 類型
    fmt.Printf("%s\n", s2)
    fmt.Printf("s-%x, c-%x, s2-%x", &s, &c, &s2)
}

控制臺輸出

字符串改變

在 Go 中字符串是不可以改變的,我們可以用上面的方法來改變字符串,這里可以看到是實現(xiàn)了 string 和 []byte 的互相轉(zhuǎn)換,達到了修改 string 的目的。我們?nèi)タ纯?string.go 的源碼會發(fā)現(xiàn),有下面的方法

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte
    if buf != nil && len(s) <= len(buf) {
        *buf = tmpBuf{}
        b = buf[:len(s)]
    } else {
        b = rawbyteslice(len(s))
    }
    copy(b, s)
    return b
}

可以看到上面有個 copy(b, s) ,這里就是將 string 復(fù)制到 []byte 上,在 slice.go 已經(jīng)實現(xiàn)過了的。從源碼中我們也可以看出每次 b 都是重新分配的,然后將 s 復(fù)制 給 b,從我們上面程控制臺輸出也可以看到每次地址都有變化,所以說 string 和 []byte 的相互轉(zhuǎn)換是有內(nèi)存開銷的,不過對于現(xiàn)在的機器來說,這點開銷也不算什么。

最后,這是我學(xué)習 Go 的 slice 的一些理解與總結(jié),由于能力有限,如果有理解不到位的地方,可以隨時留言與我交流。

參考:
1、build-web-application-with-golang
2、go

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容