您好,登錄后才能下訂單哦!
這篇文章主要介紹“go數組和切片的概念及用法”,在日常操作中,相信很多人在go數組和切片的概念及用法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”go數組和切片的概念及用法”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
每個容器(值)用來表示和存儲一個元素(element)序列或集合。一個容器中的所有元素的類型是相同的。此相同的類型稱為此容器的類型的元素類型(或簡稱此容器的元素類型)。
存儲在一個容器中的每個元素值都關聯著著一個鍵值(key)。每個元素可以通過它的鍵值而被訪問到。 一個映射類型的鍵值類型必須為一個可比較類型。
可比較類型即指支持使用==和!=運算標識符比較的類型,在Go中,除了切片類型、映射類型、函數類型、任何包含有不可比較類型的字段的結構體類型、任何元素類型為不可比較類型的數組類型之外的其它類型稱為可比較類型。
數組和切片類型的鍵值類型均為內置類型int。 一個數組或切片的一個元素對應的鍵值總是一個非負整數下標,此非負整數表示該元素在該數組或切片所有元素中的順序位置。此非負整數下標亦常稱為一個元素索引(index)。
每個容器值有一個長度屬性,用來表明此容器中當前存儲了多少個元素。 一個數組或切片中的每個元素所關聯的非負整數索引鍵值的合法取值范圍為左閉右開區間[0, 此數組或切片的長度)
。
slice 的底層數據是數組,slice 是對數組的封裝,它描述一個數組的片段。兩者都可以通過下標來訪問單個元素。
數組是定長的,長度定義好之后,不能再更改。在 Go 中,數組是不常見的,因為其長度是類型的一部分,限制了它的表達能力,比如 [3]int 和 [4]int 就是不同的類型。
而切片則非常靈活,它可以動態地擴容。切片的類型和長度無關。
數組就是一片連續的內存, slice 實際上是一個結構體,包含三個字段:長度、容量、底層數組。
slice 的數據結構如下:
// runtime/slice.go type slice struct { array unsafe.Pointer // 元素指針 len int // 長度 cap int // 容量 }
或者用圖表示:
注意:底層數組是可以被多個 slice 同時指向的,因此對一個 slice 的元素進行操作是有可能影響到其他 slice 的。
1. 示例一
[3]int 和 [4]int 是同一個類型嗎?
不是。因為數組的長度是類型的一部分,這是與 slice 不同的一點。
2. 示例二
Go中對nil的Slice和空Slice的處理是一致的嗎?
2.1. 首先Go的JSON 標準庫對 nil slice 和 空 slice 的處理是不一致的:
package main import ( "encoding/json" "fmt" "log" ) func main(){ var s1 []int s2 := []int{} b, err := json.Marshal(s1) if err != nil { log.Fatal(err) } fmt.Println(string(b)) b, err = json.Marshal(s2) if err != nil { log.Fatal(err) } fmt.Println(string(b)) }
輸出結果:
null []
2.2.通常錯誤的用法,會報數組越界的錯誤,因為只是聲明了slice,卻沒有給實例化的對象:
var slice []int slice[1] = 0
此時slice的值是nil,這種情況可以用于需要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。
2.3.empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時的定義如下:
slice := make([]int,0) slice := []int{}
當我們查詢或者處理一個空的列表的時候,這非常有用,它會告訴我們返回的是一個列表,但是列表內沒有任何值。
總之,nil slice
和 empty slice
是不同的東西,需要我們加以區分的.
3. 示例三
下面的代碼輸出是什么?
package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] /* 對于切片截取操作s[low:high:max]: 1. 第一個數(low)表示下標的起點(從該位置開始截取), 如果low取值為0表示從第一個元素開始截取; 2. 第二個數(high)表示取到哪結束,也就是下標的終點(不包含該位置), 根據公式計算(len=high-low),也就是第二個數減去第一個數,差就是數據長度。 在這里可以將長度理解成取出的數據的個數。 3. 第三個數用來計算容量,所謂容量:是指切片目前可容納的最多元素個數。 通過公式計算(cap=max-low),也就是第三個數減去第一個數。 */ s2 := s1[2:6:7] s2 = append(s2, 100) s2 = append(s2, 200) s1[2] = 20 fmt.Println(s1) fmt.Println(s2) fmt.Println(slice) }
輸出結果:
[2 3 20] [4 5 6 7 100 200] [0 1 2 3 20 5 6 7 100 9]
s1
從 slice
索引2(閉區間)到索引5(開區間,元素真正取到索引4),長度為3,容量默認到數組結尾,為8。 s2
從 s1
的索引2(閉區間)到索引6(開區間,元素真正取到索引5),容量到索引7(開區間,真正到索引6),為5。
接著,向s2
尾部追加一個元素 100:
s2 = append(s2, 100)
s2
容量剛好夠,直接追加。不過,這會修改原始數組對應位置的元素。這一改動,數組和 s1
都可以看得到。
再次向 s2
追加元素200:
s2 = append(s2, 100)
這時,s2
的容量不夠用,該擴容了。于是,s2
另起爐灶,將原來的元素復制到新的位置,擴大自己的容量。并且為了應對未來可能的 append 帶來的再一次擴容,s2
會在此次擴容的時候多留一些 buffer,將新的容量擴大為原始容量的2倍,也就是10了。
最后,修改 s1 索引為2位置的元素:
s1[2] = 20
這次只會影響原始數組相應位置的元素。它影響不到 s2
了,人家已經遠走高飛了。
再提一點,打印 s1 的時候,只會打印出 s1 長度以內的元素。所以,只會打印出3個元素,雖然它的底層數組不止3個元素。
前面我們說到,slice 其實是一個結構體,包含了三個成員:len, cap, array。分別表示切片長度,容量,底層數據的地址。
當 slice 作為函數參數時,就是一個普通的結構體。其實很好理解:若直接傳 slice,在調用者看來,實參 slice 并不會被函數中的操作改變;若傳的是 slice 的指針,在調用者看來,是會被改變原 slice 的。
值的注意的是,不管傳的是 slice 還是 slice 指針,如果改變了 slice 底層數組的數據,會反應到實參 slice 的底層數據。為什么能改變底層數組的數據?很好理解:底層數據在 slice 結構體里是一個指針,僅管 slice 結構體自身不會被改變,也就是說底層數據地址不會被改變; 但是通過指向底層數據的指針,可以改變切片的底層數據,沒有問題。
通過 slice 的 array 字段就可以拿到數組的地址。在代碼里,是直接通過類似 s[i]=10
這種操作改變 slice 底層數組元素值。
另外,值得注意的是,Go 語言的函數參數傳遞,只有值傳遞,沒有引用傳遞。
來看一個代碼片段:
package main func main() { s := []int{1, 1, 1} f(s) fmt.Println(s) } func f(s []int) { // i只是一個副本,不能改變s中元素的值 /*for _, i := range s { i++ } */ for i := range s { s[i] += 1 } }
運行一下,程序輸出:
[2 2 2]
通過下標引用的方式果真改變了原始 slice 的底層數據。
這里調用 f 函數時傳遞的是一個 slice 的副本,即在 f 函數中,s 只是 main 函數中 s 的一個拷貝。在f 函數內部,對 s 的作用并不會改變外層 main 函數的 s,正如上面所說的,在 f 函數中slice 結構體自身不會改變,即使我們通過下標引用的方式改變了切片的底層數據,其底層數據地址是不變的。
要想真的改變外層 slice,只有將返回的新的 slice 賦值到原始 slice,或者向函數傳遞一個指向 slice 的指針。我們再來看一個例子:
package main import "fmt" func myAppend(s []int) []int { // 這里 s 雖然改變了,但并不會影響外層函數的 s s = append(s, 100) return s } func myAppendPtr(s *[]int) { // 會改變外層 s 本身 *s = append(*s, 100) return } func main() { s := []int{1, 1, 1} newS := myAppend(s) fmt.Println(s) fmt.Println(newS) s = newS myAppendPtr(&s) fmt.Println(s) }
運行結果:
[1 1 1] [1 1 1 100] [1 1 1 100 100]
myAppend
函數里,雖然改變了 s
,但它只是一個值傳遞,并不會影響外層的 s
,因此第一行打印出來的結果仍然是 [1 1 1]
。
而 newS
是一個新的 slice,它是基于 s
得到的。因此它打印的是追加了一個 100 之后的結果: [1 1 1 100]
。
最后,將 newS
賦值給了 s
,s
這時才真正變成了一個新的slice。之后,再給 myAppendPtr
函數傳入一個 s
指針,這回它就真的被改變了:[1 1 1 100 100]
。
一般都是在向 slice 追加了元素之后,才會引起擴容。追加元素調用的是 append
函數。
先來看看 append
函數的原型:
func append(slice []Type, elems ...Type) []Type
append
函數的參數長度可變,因此可以追加多個值到 slice 中,還可以用 ... 傳入 slice,直接追加一個切片:
slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...)
append
函數返回值是一個新的slice,Go編譯器不允許調用了 append
函數后不使用返回值:
append(slice, elem1, elem2) append(slice, anotherSlice...)
所以上面的用法是錯的,不能編譯通過。
使用 append
可以向 slice 追加元素,實際上是往底層數組添加元素。但是底層數組的長度是固定的,如果索引 len-1
所指向的元素已經是底層數組的最后一個元素,就沒法再添加了。
這時,slice 會遷移到新的內存位置,新底層數組的長度也會增加,這樣就可以放置新增的元素。同時,為了應對未來可能再次發生的 append
操作,新的底層數組的長度,也就是新 slice 的容量是留了一定的 buffer 的。否則,每次添加元素的時候,都會發生遷移,成本太高。
新 slice 預留的 buffer 大小是有一定規律的。網上大多數的文章都是這樣描述的:
當原 slice 容量小于 1024 的時候,新 slice 容量變成原來的 2 倍;原 slice 容量超過 1024,新 slice 容量變成原來的1.25倍。
這里先說結論:以上描述是錯誤的。
為了說明上面的規律是錯誤的,看下面這段代碼:
package main import "fmt" func main() { s := make([]int, 0) oldCap := cap(s) for i := 0; i < 2048; i++ { s = append(s, i) newCap := cap(s) if newCap != oldCap { fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }
先創建了一個空的 slice
,然后,在一個循環里不斷往里面 append
新的元素。然后記錄容量的變化,并且每當容量發生變化的時候,記錄下老的容量,以及添加完元素之后的容量,同時記下此時 slice
里的元素。這樣,我就可以觀察,新老 slice
的容量變化情況,從而找出規律。
運行結果:
[0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 1024 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304
在老 slice
容量小于1024
的時候,新 slice
的容量的確是老 slice
的2倍。目前還算正確。
但是,當老 slice
容量大于等于 1024
的時候,情況就有變化了。當向 slice
中添加元素 1280
的時候,老 slice
的容量為 1280
,之后變成了 1696
,兩者并不是 1.25
倍的關系 (1696/1280=1.325)
。添加完 1696
后,新的容量 2304
當然也不是 1696
的 1.25
倍。
可見,現在網上各種文章中的擴容策略并不正確。我們直接搬出源碼:
向 slice 追加元素的時候,若容量不夠,會調用 growslice 函數,所以我們直接看它的代碼。
// go 1.14.6 src/runtime/slice.go:76 // et:slice元素類型,old:舊的slice,cap:所需的新最小容量 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 } } } // …… }
如果只看前半部分,現在網上各種文章里說的 newcap
的規律是對的。現實是,后半部分還對 newcap
作了一個內存對齊,這個和內存分配策略相關。進行內存對齊之后,新slice
的容量是要 大于等于 老 slice
容量的 2
倍或者1.25
倍。
之后,向 Go 內存管理器申請內存,將老 slice
中的數據復制過去,并且將 append
的元素添加到新的底層數組中。
最后,向 growslice
函數調用者返回一個新的 slice
,這個 slice
的長度并沒有變化,而容量卻增大了。
下面來看幾個示例:
1. 示例一
package main import "fmt" func main() { s := []int{5} s = append(s, 7) s = append(s, 9) x := append(s, 11) y := append(s, 12) fmt.Println(s, x, y) }
解釋:
代碼 | 切片對應狀態 |
---|---|
s := []int{5} | s 只有一個元素,[5] |
s = append(s, 7) | s 擴容,容量變為2,[5, 7] |
s = append(s, 9) | s 擴容,容量變為4,[5, 7, 9]。注意,這時 s 長度是3,只有3個元素 |
x := append(s, 11) | 由于 s 的底層數組仍然有空間,因此并不會擴容。這樣,底層數組就變成了 [5, 7, 9, 11]。注意,此時 s = [5, 7, 9],容量為4;x = [5, 7, 9, 11],容量為4。這里 s 不變 |
y := append(s, 12) | 這里還是在 s 元素的尾部追加元素,由于 s 的長度為3,容量為4,所以直接在底層數組索引為3的地方填上12。結果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的長度均為4,容量也均為4 |
所以最后程序的執行結果是:
[5 7 9] [5 7 9 12] [5 7 9 12]
這里要注意的是,append函數執行完后,返回的是一個全新的 slice,并且對傳入的 slice 并不影響。
2. 示例二
關于 append
,我們來看這個例子:
package main import "fmt" func main() { s := []int{1,2} s = append(s,4,5,6) fmt.Printf("len=%d, cap=%d",len(s),cap(s)) }
運行結果是:
len=5, cap=6
如果按網上各種文章中總結的那樣:小于原 slice 長度小于 1024 的時候,容量每次增加 1 倍。添加元素 4 的時候,容量變為4;添加元素 5 的時候不變;添加元素 6 的時候容量增加 1 倍,變成 8。 那上面代碼的運行結果應該是:
len=5, cap=8
這顯然是錯誤的,我們來仔細看看,為什么會這樣,再次搬出源碼:
// go 1.14.6 src/runtime/slice.go:76 // et:slice元素類型,old:舊的slice,cap:所需的新最小容量 func growslice(et *_type, old slice, cap int) slice { // …… newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { // …… } // …… // 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: // …… case et.size == sys.PtrSize: // …… capmem = roundupsize(uintptr(newcap) * sys.PtrSize) // …… newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): // …… default: // …… } }
這個函數的參數依次是 元素的類型,老的 slice
,新 slice
所需的最小容量。
例子中s
原來只有 2
個元素,len 和 cap 都為 2
,append 了三個元素后,長度變為 5
,容量最小要變成 5
,即調用 growslice 函數時,傳入的第三個參數應該為 5
。即 cap=5
。而一方面,doublecap
是原 slice容量的 2
倍,等于 4
。滿足第一個 if
條件,所以 newcap
變成了 5
。
接著調用了 roundupsize
函數,傳入 40
。(代碼中ptrSize
是指一個指針的大小,int
類型在64位機上大小是8
)
我們再看內存對齊,搬出 roundupsize
函數的代碼:
// Returns size of the memory block that mallocgc will allocate if you ask for the size. func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) } else { //…… } } //…… } const _MaxSmallSize = 32768 const smallSizeMax = 1024 const smallSizeDiv = 8
很明顯,我們最終將返回這個式子的結果:
class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]
這是 Go 源碼中有關內存分配的兩個 slice
。class_to_size
通過 spanClass
獲取 span
劃分的 object
大小。而 size_to_class8
表示通過 size
獲取它的 spanClass
。
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 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, 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, 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, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31} var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 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}
我們傳進去的 size
等于 40
。所以 (size+smallSizeDiv-1)/smallSizeDiv = 5
;獲取 size_to_class8
數組中索引為5
的元素為 4
;獲取 class_to_size
中索引為 4
的元素為 48
。 最終,新的 slice
的容量為 6
:
newcap = int(capmem / ptrSize) // 6
至于上面的兩個魔法數組的由來,就不展開了。
3. 示例三
向一個nil的slice添加元素會發生什么?為什么?
其實 nil slice
或者empty slice
都是可以通過調用 append
函數來獲得底層數組的擴容。最終都是調用 mallocgc
來向 Go 的內存管理器申請到一塊內存,然后再賦給原來的nil slice
或 empty slice
,然后搖身一變,成為“真正”的 slice
了。
綜上,本篇文章介紹了數組和切片的相關知識,并著重分析了切片的擴容原理,了解這些對于我們恰當使用切片進行開發并規避一些如底層數組改變而引發的切片問題也是有所幫助的。
參考文章:
https://qcrao91.gitbook.io/go/shu-zu-he-qie-pian
https://gfw.go101.org/article/container.html
到此,關于“go數組和切片的概念及用法”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。