Go 語(yǔ)言的 Array 和 Slice

先拋出幾個(gè)問題

  1. 聲明一個(gè) slice 并賦值為 nil, 如 var slice []int = nil,此時(shí) len(slice) 的運(yùn)行結(jié)果是什么?
  2. func(arr []int)func(arr [10]int) 兩個(gè)函數(shù)內(nèi)部都對(duì) arr 進(jìn)行修改, 對(duì)外面的值(作為參數(shù)的數(shù)據(jù))是否造成影響?
  3. 創(chuàng)建一個(gè) slice := make([]int, 5, 10), 然后 slice[8]slice[:8] 的運(yùn)行結(jié)果是什么?
  4. 下面兩段代碼的輸出結(jié)果是什么
slice := []int{1, 2, 3, 4, 5}
slice2 := append(slice[:3], 6, 7)
fmt.Println(slice)
fmt.Println(slice2)
slice := []int{1, 2, 3, 4, 5}
slice2 := append(slice[:3], 6, 7, 8) // 多追加一個(gè)數(shù)字 8, 這是唯一的不同
fmt.Println(slice)
fmt.Println(slice2)

如果上面的問題都能很輕松回答上來(lái), 可直接關(guān)閉文章.

為了方便, 下面的描述均以 int 作為元素類型說(shuō)明

數(shù)組 Array

先說(shuō)一下數(shù)組, 的確在 Go 語(yǔ)言中, 因?yàn)?slice 的存在, 使得 array 的出場(chǎng)率不高。但想要很好的理解 slice, 還是要先要了解 array.

數(shù)組的聲明

Go 語(yǔ)言的數(shù)組和其他語(yǔ)言一樣, 沒有什么特別的地方, 就是一段以元素類型(如int)為單位的連續(xù)內(nèi)存空間。數(shù)組創(chuàng)建時(shí), 被初始化為元素類型的零值.

聲明舉例:

var arr [10]int  // 長(zhǎng)度為 10 的數(shù)組, 默認(rèn)所有元素是 0
arr := [...]int{1, 2, 3} // 長(zhǎng)度由初始化元素個(gè)數(shù)指定, 這里長(zhǎng)度是 3
arr := [...]int{11: 3} // 長(zhǎng)度為 11 的數(shù)組, arr[11] 初始化為 3, 其他為 0
arr := [5]int{1,2} // 長(zhǎng)度為 5 的數(shù)組, 前兩位初始化為 1, 2
arr := [5]int{1,2} // 長(zhǎng)度為 5 的數(shù)組, 前兩位初始化為 1, 2
arr := [...]int{1: 23, 2, 3: 22} // 長(zhǎng)度為 4 的數(shù)組, 初始化為 [0 23 2 22]

[] 內(nèi)設(shè)定數(shù)組長(zhǎng)度, 寫成 ... 表示長(zhǎng)度由后面的初始化值決定.

數(shù)組初始化的完整寫法是 {1:23, 2:8, 3:12}, 只不過(guò)可以省略 index 寫成 {23, 8, 12}, index 自動(dòng)從 0 開始累加, 最大的 index 值決定數(shù)組長(zhǎng)度.

{5: 10, 11, 12, 6: 100} 是非法的, 因?yàn)樗鼤?huì)被轉(zhuǎn)換成 {5: 10, 6: 11, 7: 12, 6: 100}, 會(huì)出現(xiàn)編譯錯(cuò)誤 duplicate index in array literal: 6.

長(zhǎng)度為 0 的數(shù)組

比較特別的就是 [0]int, 長(zhǎng)度為 0 的數(shù)組. 這種不占有任何內(nèi)存空間的數(shù)據(jù)類型實(shí)際上是無(wú)意義的, 所以 Go 語(yǔ)言對(duì)此類數(shù)據(jù)特殊處理了一下, 此外還包括 struct{}, [10]struct{} 等.

看一個(gè)例子:

var (
    a [0]int
    b struct{}
    c [0]struct {
        Value int64
    }
    d [10]struct{}
    e = new([10]struct{}) // new 返回的就是指針
    f byte
)
fmt.Printf("%p, %p, %p, %p, %p, %p", &a, &b, &c, &d, e, &f)
// 0x1127a88, 0x1127a88, 0x1127a88, 0x1127a88, 0x1127a88, 0xc42000e280

前 5 個(gè)變量的內(nèi)存地址一樣, 第 6 個(gè)變量 f 有一個(gè)真實(shí)可用的內(nèi)存. 也就是說(shuō) Go 并沒有為 [0]intstruct{} 這類數(shù)據(jù)真正分配地址空間, 而是統(tǒng)一使用同一個(gè)地址空間.

這類數(shù)據(jù)結(jié)構(gòu)在 map 中經(jīng)常應(yīng)用, 比如 map[string]struct{}. 聲明這樣一個(gè) map 類型來(lái)標(biāo)記某個(gè) key 是否存在. 在 key 值很多的情況下, 要比 map[string]bool 之類的結(jié)構(gòu)節(jié)約很多內(nèi)存, 同時(shí)也減小 GC 壓力.

數(shù)組作為函數(shù)參數(shù)

文章最開始的問題中提到, func(arr [3]int) 內(nèi)部對(duì) arr 進(jìn)行修改是否會(huì)影響外面的實(shí)際值. 答案是不會(huì).

因?yàn)橐粋€(gè)數(shù)組作為參數(shù)時(shí), 會(huì)拷貝一份副本作為參數(shù), 函數(shù)內(nèi)部操作的數(shù)組與外界數(shù)組, 在內(nèi)存中根本就不是同一個(gè)地方. 是值傳遞不是引用傳遞, 這點(diǎn)可能和某些語(yǔ)言不同.

看下面代碼:

array := [3]int{1, 2, 3}
func(innerArray [3]int) {
    innerArray[0] = 8
    fmt.Printf("%p: %v\n", &innerArray, innerArray)
}(array)
fmt.Printf("%p: %v\n", &array, array)
// 0xc42000a2e0: [8 2 3]
// 0xc42000a2c0: [1 2 3]

函數(shù)內(nèi)外, 數(shù)組的內(nèi)存地址都不一樣, 自然不會(huì)有影響.

如果你想讓函數(shù)直接修改, 可以使用指針, 即 func(arr *[3]int).

切片 Slice

slice 通常用來(lái)表示一個(gè)變長(zhǎng)序列, 也是基于數(shù)組實(shí)現(xiàn)的??聪聢D:

goslice.png

圖中 Q2summer 是 slice, 實(shí)際就是對(duì)數(shù)組 months 引用, 只是記錄了引用了數(shù)組中的那些元素.

再看一下 slice 在 Go 內(nèi)部的定義.

type slice struct {
    array unsafe.Pointer // 被引用的數(shù)組中的起始元素地址
    len   int            // 長(zhǎng)度
    cap   int            // 最大長(zhǎng)度
}

我們對(duì) slice 的讀寫, 實(shí)際上操作的都是它所指向的數(shù)組.

看到了上面的 slice 數(shù)據(jù)結(jié)構(gòu), 自然就知道了以下兩點(diǎn):

值為 nil 的 slice 變量的 lencap 都是 0. 雖然它沒有指向具體某個(gè)數(shù)組(slice.array 為空), 但是它的 slice.lenslice.cap 默認(rèn)就是 0.

func(arr []int) 這種函數(shù)對(duì)參數(shù) arr 的修改, 會(huì)影響到外面數(shù)值, 因?yàn)楹瘮?shù)內(nèi)部操作的內(nèi)存與外界是同一個(gè). 這是 slice 和 array 的主要區(qū)別之一.

slice 越界

slice 是可伸縮變長(zhǎng)的, 導(dǎo)致很多人誤以為 slice 是不會(huì)越界的, 下面我們來(lái)闡述下幾種越界情況.

以上圖中右側(cè)的 summer 為例, summer[4] = "hello" 肯定會(huì)出現(xiàn) index out of range 的 panic 信息, 盡管 cap(summer) = 7, 但 summer[4] 超出了 len(summer) = 3 的范圍.

再看下面這個(gè)例子:

arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr[:3:5][:4]) // [1 2 3 4]
fmt.Println(arr[:3:5][:8]) // panic: runtime error: slice bounds out of range

arr[:3:5] 基于 arr 創(chuàng)建一個(gè) slice, len 是 3, cap 是 5; 然后再在這個(gè) slice 的基礎(chǔ)上分別創(chuàng)建一個(gè) len = 4len = 8 的 slice. 前者運(yùn)行正常, 后者因超出 cap = 5 范圍而 panic, 盡管后者實(shí)際想要的內(nèi)存并沒有超出 arr 數(shù)組范圍.

對(duì) slice 的操作記住兩點(diǎn):

  1. 數(shù)據(jù)直接訪問(slice[index])時(shí), index 值不能超過(guò) len(slice) 范圍
  2. 創(chuàng)建切片(slice[start:end])時(shí), start 和 end 指定的區(qū)間不能超過(guò) cap(slice) 范圍

所以, 文章開頭的第 3 個(gè)問題, slice[8] 會(huì) panic, 而 slice[:8] 正常返回.

append 函數(shù)

很多人以為 slice 是可以自動(dòng)擴(kuò)充的, 估計(jì)都是 append 函數(shù)誤導(dǎo)的. 其實(shí) slice 并不會(huì)自己自動(dòng)擴(kuò)充, 而是 append 數(shù)據(jù)時(shí), 該函數(shù)如果發(fā)現(xiàn)超出了 cap 限制自動(dòng)幫我們擴(kuò)的.

當(dāng)執(zhí)行 append(slice, v1, v2) 時(shí), append 函數(shù)會(huì)先檢查執(zhí)行結(jié)果的長(zhǎng)度是否會(huì)超出 cap(slice).

如果超出, 就先 make 一個(gè)更長(zhǎng)的 slice, 然后把整個(gè) slice 都 copy 到新 slice 中, 再進(jìn)行 append.

如果沒超, 直接以 len(slice) 為起始點(diǎn)進(jìn)行追加, len(slice) 會(huì)隨著 append 操作不斷擴(kuò)大, 直到達(dá)到 cap(slice) 進(jìn)行擴(kuò)充.

建議使用者盡可能的避免讓 append 自動(dòng)為你擴(kuò)充內(nèi)存. 一個(gè)是因?yàn)閿U(kuò)充時(shí)會(huì)出現(xiàn)一次內(nèi)存拷貝, 二是因?yàn)?append 并不知道需要擴(kuò)充多少, 為了避免頻繁擴(kuò)充, 它會(huì)擴(kuò)充到 2 * cap(slice) 長(zhǎng)度. 而有時(shí)我們并不需要那么多內(nèi)存.

所以在使用 slice 時(shí), 最好不要不 make, 直接 append 讓其自己擴(kuò)充; 而是先 make([]int, 0, capValue) 準(zhǔn)備一塊內(nèi)存, capValue 需要自己估計(jì)下, 盡可能確保足夠用就好.

更多閱讀

  1. https://blog.golang.org/slices
  2. https://github.com/golang/go/wiki/SliceTricks
  3. https://blog.golang.org/go-slices-usage-and-internals

感興趣的人可以閱讀下 Go 源碼中 $GOROOT/src/runtime/slice.go 這個(gè)文件, 代碼不多很好理解.

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

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

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