整明白 Golang slice 宣告方式、淺複製現象、深複製、append操作

Mr-houzi發表於2021-11-30

什麼是切片

切片(slice)是對陣列一個連續片段的引用。切片是一個引用型別,它實際並不儲存元素,它只是標識了陣列上的某一個連續片段。

陣列在記憶體中是一連串的記憶體空間,每個元素佔據一塊記憶體。

切片的資料結構是一個結構體,結構體內由三個引數。

  • Pointer 指向陣列中它要表示的片段的起始元素;
  • len 長度
  • cap 最大容量
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice示意圖:

slice1.png

宣告方式

slice 有[]T{}newmake三種宣告方式。具體有哪些區別將會根據下面例項進行分析。

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 對應的元素值也將發生變化。

slice2.png

通常,在沒有了解切片結構的開發者,會誤以為 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 將是兩個完全不同的切片,並且其內部指標也將指向兩個不同的陣列。這樣,一方的修改就不會影響另一方了。

slice3.png

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。

slice4.png

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 將得到的是指向切片的地址,它是一個指標,指向切片,而切片內部指標指向陣列。

slice5.png

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

總結

  1. slice 有[]T{}newmake三種宣告方式。
  2. slice 會在變數賦值時發生淺複製。
  3. copy() 可以讓 slice 進行深複製。
  4. append 再操作切片時,切片空閒容量不足時會發生擴容。

end!

文章來自 整明白 Golang slice 宣告方式、淺複製現象、深複製、append操作 | 猴子星球|Mr-houzi

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章