做爲一個 Go 語言新手,看到一切」詭異「的代碼都會感到好奇;好比我最近看到的幾個方法;僞代碼以下:數組
func FindA() ([]*T,error) {
}
func FindB() ([]T,error) {
}
func SaveA(data *[]T) error {
}
func SaveB(data *[]*T) error {
}
複製代碼
相信大部分剛入門 Go
的新手看到這樣的代碼也是一臉懵逼,其中最讓人疑惑的就是:markdown
[]*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
}
複製代碼
好比以上例子打印的是app
{1}
{2}
{1}
{2}
複製代碼
只有將方法修改成函數
func SaveB(data []*T) error {
t := data[0]
t.Name = "1233"
return nil
}
複製代碼
才能修改 T 的值:ui
&{1}
&{2}
&{1233}
&{2}
複製代碼
下面重點來看看 []*T
與 *[]T
的區別,這裏寫了兩個 append
函數:spa
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]
複製代碼
說明在函數傳遞過程當中,函數內部的修改可以影響到外部。code
下面咱們再看一個例子:orm
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 其實就是一個結構體,只是不能直接對外訪問。
源碼地址
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 是對數組 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
就會發現這幾個並無什麼聯繫,只是看起來很像容易唬人。