大型迷惑現場之[]*T是什麼?*[]T是什麼?*[]*T又是什麼?

2021-08-02 10:00:25
最近看到一段十分詭異的程式碼,包含了「[]*T」「*[]T」和「*[]*T」,乍一看都是一樣的,但我們仔細觀察就發現他們的不同之處。今天我們就來介紹一下golong的「[]*T」「*[]T」和「*[]*T」,瞭解一下他們之間的不同,一起來看看

作為一個 Go 語言新手,看到一切」詭異「的程式碼都會感到好奇;比如我最近看到的幾個方法;虛擬碼如下:

func FindA() ([]*T,error) {
}

func FindB() ([]T,error) {
}

func SaveA(data *[]T) error {
}

func SaveB(data *[]*T) error {
}

相信大部分剛入門 Go 的新手看到這樣的程式碼也是一臉懵逼,其中最讓人疑惑的就是:

[]*T
*[]T
*[]*T

這樣對切片的宣告,先不看後面兩種寫法;單獨看 []*T 還是很好理解的:
該切片中存放的是所有 T 的記憶體地址,會比存放 T 本身來說要更省空間,同時 []*T 在方法內部是可以修改 T 的值,而[]T 是修改不了。

func TestSaveSlice(t *testing.T) {
    a := []T{{Name: "1"}, {Name: "2"}}
    for _, t2 := range a {
        fmt.Println(t2)
    }
    _ = SaveB(a)
    for _, t2 := range a {
        fmt.Println(t2)
    }

}
func SaveB(data []T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

type T struct {
    Name string
}

比如以上例子列印的是

{1}
{2}
{1}
{2}

只有將方法修改為

func SaveB(data []*T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

才能修改 T 的值:

&{1}
&{2}
&{1233}
&{2}

範例

下面重點來看看 []*T 與 *[]T 的區別,這裡寫了兩個 append 函數:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

先看第一種,輸出是結果是:

appendA [1000 2 3]
main [1000 2 3]

說明在函數傳遞過程中,函數內部的修改能夠影響到外部。


下面我們再看一個例子:

func appendB(x []int) {
    x = append(x, 4)
    fmt.Printf("appendA %v\n", x)
}

最終結果卻是:

appendA [1 2 3 4]
main [1 2 3]

沒有影響到外部。

而當我們再調整一下會發現又有所不同:

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendA %v\n", x)
}

最終的結果:

appendA &[1 2 3 4]
main [1 2 3 4]

可以發現如果傳遞切片的指標時,使用 append 函數追加資料時會影響到外部。

slice 原理

在分析上面三種情況之前,我們先來了解下 slice 的資料結構。

直接檢視原始碼會發現 slice 其實就是一個結構體,只是不能直接對外存取。

原始碼地址 runtime/slice.go

其中有三個重要的屬性:

屬性含義
array底層存放資料的陣列,是一個指標。
len切片長度
cap切片容量 cap>=len

提到切片就不得不想到陣列,可以這麼理解:

切片是對陣列的抽象,而陣列則是切片的底層實現。

其實通過切片這個名字也不難看出,它就是從陣列中切了一部分;相對於陣列的固定大小,切片可以根據實際使用情況進行擴容。

所以切片也可以通過對陣列"切一刀"獲得:

x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))

其中 x1 的長度與容量都是6。

x2 的長度與容量則為3和5。

  • x2 的長度很容易理解。

  • 容量等於5可以理解為,當前這個切片最多可以使用的長度。

因為切片 x2 是對陣列 x1 的參照,所以底層陣列排除掉左邊一個沒有被參照的位置則是該切片最大的容量,也就是5。

同一個底層陣列

以剛才的程式碼為例:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

在函數傳遞過程中,main 中的 x 與 appendA 函數中的 x 切片所參照的是同個陣列。

所以在函數中對 x[0]=100,main函數中也能獲取到。

本質上修改的就是同一塊記憶體資料。

值傳遞帶來的誤會

在上述例子中,在 appendB 中呼叫 append 函數追加資料後會發現 main 函數中並沒有受到影響,這裡我稍微調整了一下範例程式碼:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
主要是修改了切片初始化方式,使得容量大於了長度,具體原因後續會說明。

輸出結果如下:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5

main 函數中的資料看樣子確實沒有受到影響;但細心的朋友應該會注意到 appendB 函數中的 x 在 append() 之後長度 +1 變為了4。

而在 main 函數中長度又變回了3.

這個細節區別就是為什麼 append() "看似" 沒有生效的原因;至於為什麼要說「看似」,再次調整了程式碼:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}

在剛才的基礎之上,以 append 之後的 x 為基礎再做了一個切片;該切片的範圍為 x 所參照陣列的全部資料。

再來看看執行結果如何:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5

會神奇的發現 y 將所有資料都列印出來,在 appendB 函數中追加的資料其實已經寫入了陣列中,但為什麼 x 本身沒有獲取到呢?

看圖就很容易理解了:

  • 在appendB中確實是對原始陣列追加了資料,同時長度也增加了。

  • 但由於是值傳遞,所以 slice 這個結構體即便是修改了長度為4,也只是對複製的那個物件修改了長度,main 中的長度依然為3.

  • 由於底層陣列是同一個,所以基於這個底層陣列重新生成了一個完整長度的切片便能看到追加的資料了。

所以這裡本質的原因是因為 slice 是一個結構體,傳遞的是值,不管方法裡如何修改長度也不會影響到原有的資料(這裡指的是長度和容量這兩個屬性)。

切片擴容

還有一個需要注意:

剛才特意提到這裡的例子稍有改變,主要是將切片的容量設定超過了陣列的長度;

如果不做這個特殊設定會怎麼樣呢?

func TestAppendB(t *testing.T) {
    x:=[]int{1,2,3}
    //x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}

輸出結果:

appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3

這時會發現 main 函數中的 y 切片資料也沒有發生變化,這是為什麼呢?

這是因為初始化 x 切片時長度和容量都為3,當在 appendB 函數中追加資料時,會發現沒有位置了。

這時便會進行擴容:

  • 將老資料複製一份到新的陣列中。

  • 追加資料。

  • 將新的資料記憶體地址返回給 appendB 中的 x .

同樣的由於是值傳遞,所以 appendB 中的切片換了底層陣列對 main 函數中的切片沒有任何影響,也就導致最終 main 函數的資料沒有任何變化了。

傳遞切片指標

有沒有什麼辦法即便是在擴容時也能對外部產生影響呢?

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendC %v\n", x)
}

輸出結果為:

appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6

這時外部的切片就能受到影響了,其實原因也很簡單;

剛才也說了,因為 slice 本身是一個結構體,所以當我們傳遞指標時,就和平時自定義的 struct 在函數內部通過指標修改資料原理相同。

最終在 appendC 中的 x 的指標指向了擴容後的結構體,因為傳遞的是 main 函數中 x 的指標,所以同樣的 main 函數中的 x 也指向了該結構體。

總結

所以總結一下:

  • 切片是對陣列的抽象,同時切片本身也是一個結構體。

  • 引數傳遞時函數內部與外部參照的是同一個陣列,所以對切片的修改會影響到函數外部。

  • 如果發生擴容,情況會發生變化,同時擴容會導致資料拷貝;所以要儘量預估切片大小,避免資料拷貝。

  • 對切片或陣列重新生成切片時,由於共用的是同一個底層陣列,所以資料會互相影響,這點需要注意。

  • 切片也可以傳遞指標,但場景很少,還會帶來不必要的誤解;建議值傳值就好,長度和容量佔用不了多少記憶體。

相信使用過切片會發現非常類似於 Java 中的 ArrayList,同樣是基於陣列實現,也會擴容發生資料拷貝;這樣看來語言只是上層使用的選擇,一些通用的底層實現大家都差不多。

這時我們再看標題中的 []*T *[]T *[]*T 就會發現這幾個並沒有什麼聯絡,只是看起來很像容易唬人。

有需要的可以看教學哦

以上就是大型迷惑現場之[]*T是什麼?*[]T是什麼?*[]*T又是什麼?的詳細內容,更多請關注TW511.COM其它相關文章!