聽說你還搞不懂Golang的Slice?看這一篇就夠了!

2022-01-07 NightTeam

在前面的文章中,我和大家一起學習了一下關於 Go 語言中數組的知識,當時有提到過一個知識點:在函數中傳遞數組是非常耗資源的一件事,所以更推薦大家使用切片(slice)來這麼做。

那麼切片又是一個怎樣的東西呢?看完這篇文章你就知道了!

上一篇傳送門:Go by Example-圖解數組

初識切片

切片(slice)也是一種數據結構,它和數組非常相似,但它是圍繞動態數組的概念設計的,可以按需自動改變大小。

切片的動態增長是通過內置函數 append 來實現的,這個函數可以快速且高效地增長切片。

使用切片後,不僅可以更方便地管理和使用數據集合,還可以在切片的基礎上繼續使用切片來縮小一個切片的長度範圍。

因為切片的底層就是一個數組,所以切片和數組的一些操作類似。比如:獲得切片索引、迭代切片等。

切片的對象非常小,它是一個只有 3 個欄位的數據結構,分別是:

1.指向底層數組的指針2.切片的長度3.切片的容量

了解了一下切片的好處和特性之後,我們再來看看如何創建切片吧。

創建切片

創建切片的方式有兩種,一種是使用內置的 make 函數,一種是使用切片的字面量。

下面我們來看一下這兩種創建方式的使用方法。

使用 make 創建

在創建之前我們先簡單地了解一下 make 函數的作用。

make 只能應用於三種數據類型:本文中的 slice、以及後面要說的 map 和 chan。

make 會為它們分配內存、初始化一個對應類型的對象並返回。注意,返回值的類型依然是被 make 的那個類型,make 只對其做了一個引用。

make 主要是用來做初始化的,記住這點,這在後面的學習中非常重要(但不是本章的後面)。

使用長度和容量聲明整型切片
slice := make([]int, 4, 5)

在使用 make 創建切片的時候,一共可以傳入三個參數:

1.聲明一個 int 類型的切片2.指定切片的長度3.指定切片的容量,也就是底層數組的長度

由於切片可以按需改變大小,所以在聲明類型的時候並不需要指定長度(即[123]int 這種寫法),也不需要讓編譯器自己去推斷數組的大小(即[...]int 這種寫法)。

所謂的容量其實就是切片可以增加到的最大長度。如果基於這個切片創建新的切片,新切片會和原有切片共享底層數組,也能通過後期操作來訪問多餘容量的元素。

長度和容量一樣的情況

如果你在使用 make 函數創建切片的時候只使用了兩個參數的話,那麼這個切片的長度和容量將會是一樣的。

例如這裡我聲明了一個 string 類型的切片,並將它的長度和容量都設置為 6。

slice := make([]string, 6)

注意:切片的容量不能小於其長度
slice: = make([]int, 3, 1)

這行代碼在編譯的時候是會報錯的,因為切片的容量不能小於長度,需要注意。

通過字面量創建
// 創建一個字符串切片並賦值,其長度和容量都是5。slice := []string {"I", "Love", "My", "祖", "國"}
// 創建一個整型切片並賦值,其長度和容量都是3。slice := []int {10, 20, 30}

另外,在使用切片字面量時,我們可以設置初始長度和容量。

// 創建一個整形切片,使用整形數字 8 初始化第 10 個元素slice := []int {9: 8}// 註:上面的這個 9 代表下標 9,也就是第 10 個元素

上面的代碼表示聲明一個長度和容量都為 10 的數組,並把第 10 個元素的值設定為 8,其他位置的元素此時因為沒有賦值,所以是對應類型的零值。這裡的話因為聲明類型是 int 的關係,所以是一個 int 值的 0。

用哪種方式創建呢?

這個主要得看在聲明切片的時候,是否知道裡面部分元素的值。

如果不知道的話,不管使用哪種方式都可以。

但如果想在聲明的時候順帶給元素賦值,那麼就可以選擇使用字面量的方式。

為什麼切片會和底層數組有關係呢?

這個實際上和它的數據結構聲明是有關係的,切片實際上是一個結構體類型的數據結構,看一下切片類型的源碼就知道了:

type slice struct {    array unsafe.Pointer // 底層數組    len int    cap int}

不過由於本文主要說的是切片的關係,結構體等內容就先暫且不談了,現在我們只需要知道切片底層有個數組就可以了。

圖解切片結構

下面通過一張圖來對切片做一個直觀的理解,就拿下面這個例子來說:

slice := make([]int, 4, 5)slice[2] = 9

把它畫成圖的話就是這個樣子的:

因為切片存的只是指向底層數組的指針而已,所以切片佔用的內寸空間是很小的。

我們來做個簡單的計算,我的電腦是 64 位的,unsafe.Pointer 類型變量佔用 8 個字節,int 類型變量佔用 8 個字節,所以算起來這個切片所佔的內存空間大小只有 8 + 8 * 2 = 24 字節,非常小。

操作切片通過切片創建切片

除了上面的方式以外,我們還可以使用切片來創建切片。

// 創建一個整型切片,其長度和容量都是 5 個元素slice := []int{1, 2, 3, 4, 5}
// 創建一個新切片,其長度為 2 個元素,容量為 4 個元素newSlice := slice[1:3]

執行完這段操作以後,我們就有兩個切片了,它們共享一個底層數組,我們通過一個圖來幫助理解一下這個過程。

新切片的下標 [0] 對應的實際是底層數組的下標 [1]。

如果沒有限定容量的大小,那麼可以得知:

•新切片的長度為 3-1=2。•新切片的容量為 5-1=4。(這個5代表原來切片總長度)

說到容量,還需要注意一點,它一般只是用來增加切片長度用的,我們無法通過下標去取裡面的內容。

例如:

package main
import "fmt"
func main () { slice := make([]int, 4, 5) newSlice := slice[1:3] fmt.Println(newSlice[1]) fmt.Println(newSlice[2])}

運行結果會是:

panic: runtime error: index out of range

使用切片創建新的切片的時候,實際上一共是有三個參數的,前面我們已經使用了兩個,現在我們來看看第三個參數。

第三個參數是用來限定新的切片的最大容量的,這個最大容量計算是從索引位置開始,加上希望容量中包含的元素的個數得到的。

舉個例子:

// 創建一個整型切片,其長度和容量都是 5 個元素slice := []int{1, 2, 3, 4, 5}
// 創建一個新切片,其長度為 3-1=2 個元素,容量為 4-1=3 個元素newSlice := slice[1:3:4]

這裡我們要獲取的新切片要求是從底層數組索引 1 的位置開始,然後取其後面最多 3 個數。

執行後得到的就是我們要寫的第三個參數:4。

此時如果用圖表示的話是這個樣子:

灰色部分是新切片不能拓展到的部分,原因很簡單,因為我們把最大容量設置為了 3。

注意:如果設置的容量比可用的容量還大,就會得到一個語言運行時錯誤。

nil 切片

在聲明切片時不做任何初始化,就會創建一個 nil 切片,nil 切片可以用於很多標準庫和內置函數。

// 創建 nil 整型切片var slice [] int

nil 切片長度為 0,容量也為 0。

那麼這個東西有什麼用呢?

比如說我們調用了一個函數,我們希望它返回一個切片,但是運行期間發生了異常,這個時候我們就可以通過判斷結果是否為 nil,得知程序是否出現異常了。

切片賦值

對切片賦值就很簡單了,直接通過它的下標進行賦值即可。舉個例子:

slice := []int{10, 20, 30}slice[1] = 3

得到結果如下:

注意:賦值的時候使用的索引,不能超過切片的最大索引,也就是切片的長度 - 1。

如果你需要給通過切片創建的切片進行賦值,那麼你需要注意了!前面說過新舊切片是共享同一個底層數組的,而修改切片的值實際上是在修改底層數組的值,所以這就產生了一個問題,如果對新的切片賦值,那麼舊的切片的值也會發生變化。

舉個例子:

package main
import "fmt"
func main () { slice := make ([]int, 4, 5) newSlice := slice[1:3] fmt.Printf("舊切片賦值之前:% d\n", newSlice) fmt.Printf("新切片賦值之前:% d\n", slice) newSlice[1] = 666 fmt.Printf("舊切片賦值之前:% d\n", slice) fmt.Printf("新切片賦值之前:% d\n", newSlice)}

得到結果如下:

新切片賦值之前:[0 0]舊切片賦值之前:[0 0 0 0]新切片賦值之後:[0 0 666 0]舊切片賦值之後:[0 666]

看到了嗎?舊切片的值也被改變了!那麼同理,如果對舊切片賦值,新切片也是會發生變化,這個就不做演示了,和上面這個結果類似。

切片增長

Go 語言內置的 append 函數可以增加切片的長度,使用 append 需要一個被操作的切片和一個要追加的值。

append 必定會增加新切片的長度,而切片容量的變化則取決於被操作的切片的可用容量,可增長可不增長。

簡單來說,如果 append 操作完之後,切片內的元素個數不大於容量數,那麼新的切片就不增加容量。同樣,此時的新切片還是和之前的切片共享同一個底層數組。但是這種做法我並不建議你使用!

舉個例子:

package main
import "fmt"
func main() { slice := []int{1,2,3,4,5} newSlice := slice[1:3:4] fmt.Printf("新切片之前元素:% d\n", newSlice) fmt.Printf("舊切片之前元素:% d\n", slice) newSlice = append(newSlice, 6) fmt.Printf("舊切片增加之後元素:% d\n", slice) fmt.Printf("新切片此時元素:% d\n", newSlice)

得到結果如下:

新切片賦值之前:[2 3]舊切片賦值之前:[1 2 3 4 5]舊切片增加之後元素:[1 2 3 6 5]新切片此時元素:[2 3 6]

讓我們通過下面這張圖來分析一下,上面都發生了什麼。

看圖可以發現,我們在增加新切片的元素的時候,無意中修改了底層的數組元素,這導致舊的切片的值也發生了變化,所以我更推薦你使用下面這種方式對新的切片進行元素的增加:

還記得前面說的:「使用切片創建切片的時候,可以使用第三個參數限制其最大容量」麼?

這裡我們要說的就是使用 append 之後,新的切片容量增大的情況。

如果切片的底層數組沒有足夠的可用容量,append 函數會創建一個新的底層數組,然後將被引用的現有的值複製到新數組裡,再追加新的值。

也就是說,這種情況新的切片可以單獨享用底層的數組,這樣的話即使你修改了新切片的值,對舊的切片也不會造成任何影響,因為它們不再共享同一個底層數組。

舉個例子:

slice := []int{1, 2, 3, 4, 6}newSlice := append(slice, 6)

當這個 append 操作完成後,newSlice 會擁有一個全新的底層數組,這個數組的容量是原來的兩倍。

函數 append 會智能地處理底層數組的容量增長,在切片的容量小於 1000 個元素時,它總是會成倍地增加容量。一旦元素個數超過 1000,容量的增長倍數就會被設為 1.25,也就是說會每次增加 25% 的容量。

append 除了可以添加元素以外,還可以在一個切片中追加另一個切片。只需要通過 ... 來將第二個切片的元素做一個拆分就行了,這裡的 ... 就類似於 Python 裡的解包操作。

s1 := []int {1, 2}s2 := []int {3, 4}
fmt.Printf("% v\n", append(s1, s2...))
輸出:[1 2 3 4]

切片迭代

對了,切片還可以像數組那樣去迭代,只需要這樣就行了:

slice := []int {1, 2, 3, 4}for index, value := range slice {    fmt.Printf("Index: % d Value: % d\n", index, value)}

在函數間傳遞切片

切片在函數間進行傳遞的時候,只是複製了切片的本身,不會涉及底層的數組。

前面我們計算了一下,在 64 位的機器上,切片僅佔用了 24 個字節,而在函數間傳遞 24 字節的數據是非常簡單、快速的事情。

這正是切片效率高的地方,不需要傳遞指針,也不需要處理複雜的語法,只需要複製切片,按想要的方式修改數據,然後傳遞迴一份新的切片副本就可以了,非常的簡單快捷。

參考資料

•https://www.jb51.net/article/126703.htm•https://www.cnblogs.com/chenpingzhao/p/9918062.html•https://blog.csdn.net/qq_19018277/article/details/100578553•http://www.meirixz.com/archives/80658.html

文章作者:「夜幕團隊 NightTeam」 - 陳祥安

潤色、校對:「夜幕團隊 NightTeam」 - Loco

夜幕團隊成立於 2019 年,團隊包括崔慶才、周子淇、陳祥安、唐軼飛、馮威、蔡晉、戴煌金、張冶青和韋世東。

涉獵的程式語言包括但不限於 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發、對象存儲等。團隊非正亦非邪,只做認為對的事情,請大家小心。

相關焦點

  • 徹底理解Golang Slice
    看完這篇文章,下面這些高頻面試題你都會答了吧實現原理slice是無固定長度的數組,底層結構是一個結構體,包含如下3個屬性一個 slice 在 golang 中佔用 24 個 bytestype 「golang中通過語法糖,使得我們可以像聲明array一樣,自動創建slice結構體」根據索引位置取切片slice 元素值時,默認取值範圍是(0~len(slice)-1),一般輸出slice時,通常是指 slice[0:len(slice)-1],根據下標就可以輸出所指向底層數組中的值主要特性引用類型
  • 從Golang Slice的內存洩漏來理解Slice的使用邏輯
    Golang雖然是自帶GC的語言,仍然存在內存洩漏的情況,
  • Golang【源碼系列】深入了解slice
    不過這也側面證明了Go語言設計所追求的目標:簡單性和可讀性,看到定義或者代碼,能讓人直接明白這段代碼的含義,相當了不起。言歸正傳,這裡還有一些細節可以補充。slice的內存布局了解了slice的結構,我們來直觀的看一個slice實例在內存中的布局:取a的切片[3:6]賦值為c,c對應的內存布局如下:取切片(截取)語法為左閉右開區間,複製的切片長度即為新切片的len,這個很好理解,但是新切片的cap為什麼是5?
  • Golang 基礎整理 這一篇就夠了
    第一個golang程序package mainimport "fmt"func main() {    fmt.Println("hello golang")}基本數據類型布爾型( true 或者 false)數字類型( 整型 int 和 浮點型 float32、float64 )
  • 一種理解 Golang Slice 的模型
    【導讀】本文以圖示的方式給出一種理解 slice 的模型的方法,分析了在特殊場景的slice用法。概述Golang 中 slice 極似其他語言中數組,但又有諸多不同,因此容易使初學者產生一些誤解,並在使用時不易察覺地掉進各種坑中。
  • 關於Golang切片Slice和append的有趣問題
    以及想想為什麼會這樣子?其中的知識點有哪些?/【4】    for i := 0; i < 10; i++ {     y = append(y, i)    }    fmt.Println("y=", y, &y[0]) //y= [20 2 10 20 0 1 2 3 4 5 6 7 8 9] 0xc0000ba000 【5】}解釋【1】因為y是x的slice
  • 「語言學習」Go語言之slice特性
    Go語言學習slice介紹和說明golang的數據結構也很多,如List,array,map等,但是有個很特別的數據結構是slice,也叫切片那麼什麼是slice呢?其實slice也算是golang語言特有的數據結構,底層是以數組作為支撐;啥概念呢,就是說在申請一塊內存進行數組的存放的時候,slice就像數組對外開放的一扇窗口,讓你看到想給你看到的內容。
  • Golang 筆記(三):一種理解 Slice 的模型
    本篇小文,首先從 Go 語言官方博客出發,鋪陳官方給出的 slice 的相關語法;其次以圖示的方式給出一種理解 slice 的模型;最後再總結分析一些特殊的使用情況,以期在多個角度對 slice 都有個更清晰側寫。如不願看繁瑣敘述過程,可直接跳到最後小結看總結。
  • Golang slice 使用技巧
    寫了一個性能測試(源碼在GitHub),看一下效果吧:goos: darwingoarch: amd64pkg: github.com/thinkeridea/example/sliceBenchmarkSliceInt2String1-8     3000000
  • golang標準庫template
    eq Returns the boolean truth of arg1 == arg2ne Returns= arg2lt Returns the boolean truth of arg1 < arg2le Returns the boolean
  • Golang 性能測試 (1)
    ) { slice := ints[0:100] t.ResetTimer() for i := 0; i < t.N; i++ { sort.Ints(slice) }}// 長度為 1w 的數據使用上述代碼排序func BenchmarkQsort10k(t *testing.B) { slice := ints
  • 「Golang」for range 使用方法及避坑指南
    ,看runtime.mapiterinit和runtime.mapiternext這兩個鬼東西到底怎麼實現的。我將剔除一些無用代碼,如race相關代碼首先看runtime.mapiterinit:// 傳入參數是,map的類型,對應的map,以及迭代器的變量func mapiterinit(t *maptype, h *hmap, it *hiter) { // 空map你遍歷個p啊 if
  • 你能一口說出go中字符串轉字節切片的容量嘛?
    前一篇文章這種時候只好祭出前一篇文章的套路了, 看看彙編代碼(希望之後有機會能夠對go的彙編語法進行簡單的介紹)有沒有什麼關鍵詞能夠幫助我們以下為現象一轉換的彙編代碼關鍵部分以下為現象二轉換的彙編代碼關鍵部分在看彙編代碼之前, 我們首先來看一看runtime.stringtoslicebyte
  • 獲得了「官方自己都會踩的」坑認證:slice 類型內存洩露的邏輯
    這裡看 Go 的基本類型:map 和 slice。sync.Pool 在復用對象時,需要我們在 Get 或 Put 時對對象進行清空操作,這個清空是由用戶完成的,對於一個 slice 來說,清空操作很簡單:sl := sl[:0]。但對於一個 map 來說,就沒這麼簡單了。
  • 跟 Dave Cheney 大神重學 Go Slice:有新收穫
    作者註:這有時也被成為後臺數組(backing arrays)。Go 中聲明一個結構體的時候,你都會傳遞一個結構體指針。slice 的值傳遞很不常見,我能想到的另一個值傳遞的結構體為 time.time。作者註:當結構體實現了某個接口的時候,那麼傳遞指針的概率這接近 100%。
  • Golang最細節篇— struct{} 空結構體究竟是啥?
    這句話是沒有錯的,但是更準確的來說,其實是有一個特殊起點的,那就是 zerobase 變量,這是一個 uintptr 全局變量,佔用 8 個字節。當在任何地方定義無數個 struct {} 類型的變量,編譯器都只是把這個 zerobase 變量的地址給出去。換句話說,在 golang 裡面,涉及到所有內存 size 為 0 的內存分配,那麼就是用的同一個地址 &zerobase 。
  • Golang入門教程——基本操作篇
    今天是Golang專題的第四篇,這一篇文章將會介紹golang當中的函數、循環以及選擇判斷的具體用法。函數在之前的文章當中其實我們已經接觸過函數了,因為我們寫的main函數本質上也是一個函數。只不過由於main函數沒有返回值,也沒有傳參,所以省略了很多信息。
  • 小學英語——疑問代詞搞不懂?看這一篇就夠了!
    小學階段的疑問代詞是孩子見得,說的做多的,往往在高年級孩子做題的時候搞不清楚,容易混淆以至於特別簡單的題目也容易出錯,為了從根本上解決這個難題,我將小學英語階段所接觸的疑問代詞單獨拎出進行詳細的講解,希望對孩子們理解小學英語疑問代詞有幫助。
  • Golang 需要避免踩的 50 個坑(一)
    最近準備寫一些關於golang的技術博文,本文是之前在GitHub上看到的golang技術譯文,感覺很有幫助,先給各位讀者分享一下。
  • 《8小時轉職Golang工程師》
    置頂 本視頻偏入門級,主要是針對後端想快速低成本掌握golang開發人群學習,如您已經掌握golang請繞行。