什麼是切片
切片(slice)是對陣列一個連續片段的引用。切片是一個引用型別,它實際並不儲存元素,它只是標識了陣列上的某一個連續片段。
陣列在記憶體中是一連串的記憶體空間,每個元素佔據一塊記憶體。
切片的資料結構是一個結構體,結構體內由三個引數。
- Pointer 指向陣列中它要表示的片段的起始元素;
- len 長度
- cap 最大容量
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice示意圖:
宣告方式
slice 有[]T{}
、new
、make
三種宣告方式。具體有哪些區別將會根據下面例項進行分析。
sl := []string{"a", "b", "c", "d"}
sl := make([]string, 4)
sl := new([]string)
*sl = make([]string, 4)
淺複製現象
賦值過程中發生的淺複製
來看例項程式碼
func example1a() {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
// 淺複製1:賦值過程中發生的淺複製
sl1 := sl
fmt.Printf("sl1:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl1, sl1, &sl1)
sl1[0] = "a被修改"
fmt.Println("================ sl1 被修改後 ================")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
fmt.Printf("sl1:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl1, sl1, &sl1)
}
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl1:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc0000040c0
================ sl1 被修改後 ================
sl:[a被修改 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl1:[a被修改 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc0000040c0
sl 宣告得到了一個切片,並在建立了一個陣列,sl 切片的內部指標指向這個陣列。
sl1 由 sl 賦值而來,sl1 得到了一個和 sl 一樣的切片,同樣它的內部指標也指向最初建立的陣列。
當對 sl1 的索引 0 進行修改後,列印 sl 對應的元素值也將發生變化。
通常,在沒有了解切片結構的開發者,會誤以為 sl1 與 sl 是完全獨立,互相的修改並不影響對方。實際上,它們確實是兩個完全獨立的記憶體,但是它們的內部結構都指向了同一個陣列。
切片並不儲存陣列元素,它只是搬運工,標識了陣列上的片段區間。
所以, sl1[0] 的修改實際上是修改的 sl1 索引0 對應的在陣列上的元素值。當訪問 sl 時,它讀取自己在陣列上的片段時,也將受到影響。
這一現象也被稱之為淺複製
。
函式形參中發生的淺複製
淺複製
不只發生在變數賦值過程中,在呼叫函式實參傳給形參的時候也在悄然發生。
func example1b() {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
// 淺複製2:函式形參中發生的淺複製
func (slParam []string) {
fmt.Printf("slParam:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", slParam, slParam, &slParam)
slParam[0] = "a被修改"
fmt.Println("================ slParam 被修改後 ================")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
fmt.Printf("slParam:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", slParam, slParam, &slParam)
}(sl)
// 外部的 sl 也將受到變化
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
}
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
slParam:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc0000040c0
================ slParam 被修改後 ================
sl:[a被修改 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
slParam:[a被修改 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc0000040c0
sl:[a被修改 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
在函式內部對形參切片的修改,將影響函式外實參。看過例項1a後,相信你對於這個結果並不會太震驚。
切片實參和形參是兩個不同變數,但它們擁有同樣的內部結構,內部結構中的指標依然是分別指向陣列。
深複製操作
例項1a和1b中展示了切片的淺複製現象,對於如何解決淺複製問題在本例中將會解答。
func example3() {
sl := []string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
// 深複製:透過 copy 解決賦值過程中發生的淺複製
sl2 := make([]string, 4)
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
copy(sl2, sl)
fmt.Println("================ copy 複製後 ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
sl2[0] = "a被修改了"
fmt.Println("================ sl2 被修改後 ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
}
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl2:[ ] 變數(或變數結構某個指標)指向地址(變數值):0xc0000200c0 變數地址:0xc0000040c0
================ copy 複製後 ================
sl2:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc0000200c0 變數地址:0xc0000040c0
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
================ sl2 被修改後 ================
sl2:[a被修改了 b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc0000200c0 變數地址:0xc0000040c0
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
本例中透過 copy 方法深複製
操作解決了賦值過程中的淺複製
現象。sl2 和 sl 將是兩個完全不同的切片,並且其內部指標也將指向兩個不同的陣列。這樣,一方的修改就不會影響另一方了。
append 操作
本例中展示了 append
操作。
func example4() {
sl := []string{"a", "b", "c", "d"}
sl2 := sl
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
fmt.Println("================ 陣列每個元素對應的地址 ================")
fmt.Printf("a:%p b:%p c:%p d:%p \n", &sl[0], &sl[1], &sl[2], &sl[3])
sl2 = sl2[1:2]
fmt.Println("================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
sl2 = append(sl2[:1], "e")
fmt.Println("================ 切片還有空閒容量進行 append e ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
sl2 = append(sl2, "f")
fmt.Println("================ 切片還有空閒容量進行 append f ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
sl2 = append(sl2, "g")
fmt.Println("================ 切片沒有空閒容量進行 append g ================")
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
fmt.Println("================ 發生擴容後 ================")
fmt.Printf("sl2:cap:%d,len:%d\n", cap(sl2), len(sl2))
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
sl2 = sl2[:6]
fmt.Printf("sl2:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl2, sl2, &sl2)
fmt.Println("================ 新陣列每個元素對應的地址 ================")
fmt.Printf("b:%p c:%p e:%p f:%p \n", &sl2[0], &sl2[1], &sl2[2], &sl2[3])
}
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl2:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004090
sl2:cap:4,len:4
================ 陣列每個元素對應的地址 ================
a:0xc000020080 b:0xc000020090 c:0xc0000200a0 d:0xc0000200b0
================ sl2[1:2] 使切片 sl2 指向了 b 元素 ================
sl2:[b] 變數(或變數結構某個指標)指向地址(變數值):0xc000020090 變數地址:0xc000004090
sl2:cap:3,len:1
================ 切片還有空閒容量進行 append e ================
sl2:[b e] 變數(或變數結構某個指標)指向地址(變數值):0xc000020090 變數地址:0xc000004090
sl:[a b e d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
================ 切片還有空閒容量進行 append f ================
sl2:[b e f] 變數(或變數結構某個指標)指向地址(變數值):0xc000020090 變數地址:0xc000004090
sl:[a b e f] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl2:cap:3,len:3
================ 切片沒有空閒容量進行 append g ================
sl2:[b e f g] 變數(或變數結構某個指標)指向地址(變數值):0xc00004e060 變數地址:0xc000004090
sl:[a b e f] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
================ 發生擴容後 ================
sl2:cap:6,len:4
sl2:[b e f g] 變數(或變數結構某個指標)指向地址(變數值):0xc00004e060 變數地址:0xc000004090
sl2:[b e f g ] 變數(或變數結構某個指標)指向地址(變數值):0xc00004e060 變數地址:0xc000004090
================ 新陣列每個元素對應的地址 ================
b:0xc00004e060 c:0xc00004e070 e:0xc00004e080 f:0xc00004e090
1.最初切片剛建立的時候,sl、sl2 切片內部指標指向陣列第一個元素a。
2.經過 sl2 = sl2[1:2]
後,sl2 指向了陣列中的第二個 b 元素。
3.往 sl2 切片 append e 時,此時 sl2 還有空閒空間(cap-len>0),append 操作直接修改了陣列元素 c => e。
4.往 sl2 切片 append f 時,此時 sl2 依然還有空閒空間(cap-len>0),append 操作直接修改了陣列元素 d => f。
5.往 sl2 切片 append g 時,此時 sl2 已經沒有空閒空間了(cap-len=0),append 操作會導致擴容。由於陣列空間是固定不變的,擴容將使 sl2 指向新的陣列。sl2 第一個元素仍然是 b,但它指向地址已經不再是最初陣列中元素b的地址了,這一點可以證明發生了擴容,併產生了新陣列。
實際上 sl2 僅需要 4 個空間,對應的新陣列卻提供了 6 個空間,至於這點應該和切片的擴容機制有關,後續文章可能會繼續深入探討。
其餘幾種 slice 宣告和操作方式
&[]T
sl := &[]string{"a", "b", "c", "d"}
// 等價於
s := []string{"a", "b", "c", "d"}
sl := &s
sl 將得到的是指向切片的地址,它是一個指標,指向切片,而切片內部指標指向陣列。
func example2() {
sl := &[]string{"a", "b", "c", "d"}
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
sl1 := sl
fmt.Printf("sl1:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl1, sl1, &sl1)
*sl1 = append(*sl1, "e")
fmt.Println("================ append 後 ================")
fmt.Printf("sl1:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl1, sl1, &sl1)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
(*sl1)[0] = "a被修改"
fmt.Println("================ sl1 被修改後 ================")
fmt.Printf("sl1:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl1, sl1, &sl1)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
}
sl:&[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
sl1:&[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006038
================ append 後 ================
sl1:&[a b c d e] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006038
sl:&[a b c d e] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
================ sl1 被修改後 ================
sl1:&[a被修改 b c d e] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006038
sl:&[a被修改 b c d e] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
make
make 方式建立切片,make 初始化了陣列空間大小,元素初始值預設為零值。
func example5() {
sl := make([]string, 4)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
sl[0] = "a"
sl[1] = "b"
sl[2] = "c"
sl[3] = "d"
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
sl = append(sl, "e", "f", "g", "h")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
}
sl:[ ] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl:[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000020080 變數地址:0xc000004078
sl:[a b c d e f g h] 變數(或變數結構某個指標)指向地址(變數值):0xc00010a000 變數地址:0xc000004078
new
new 建立切片將返回地址,sl 此時拿到的僅是地址,切片對應的陣列甚至都沒有初始化,此時無法使用這個切片。
直到經過*sl = make([]string, 4)
,之後才能正常透過指標操作切片。
func example6() {
sl := new([]string)
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
// new 只拿到了一個指標,並沒法使用這個slice,必須經過 make 初始化後,才能使用
*sl = make([]string, 4)
fmt.Println("================ make 後 ================")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
(*sl)[0] = "a"
(*sl)[1] = "b"
(*sl)[2] = "c"
(*sl)[3] = "d"
fmt.Println("================ 賦值後 ================")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
*sl = append(*sl, "b")
fmt.Println("================ append 後 ================")
fmt.Printf("sl:%+v 變數(或變數結構某個指標)指向地址(變數值):%p 變數地址:%p\n", sl, sl, &sl)
}
sl:&[] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
================ make 後 ================
sl:&[ ] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
================ 賦值後 ================
sl:&[a b c d] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
================ append 後 ================
sl:&[a b c d b] 變數(或變數結構某個指標)指向地址(變數值):0xc000004078 變數地址:0xc000006028
總結
- slice 有
[]T{}
、new
、make
三種宣告方式。 - slice 會在變數賦值時發生淺複製。
- copy() 可以讓 slice 進行深複製。
- append 再操作切片時,切片空閒容量不足時會發生擴容。
end!
文章來自 整明白 Golang slice 宣告方式、淺複製現象、深複製、append操作 | 猴子星球|Mr-houzi
本作品採用《CC 協議》,轉載必須註明作者和本文連結