1. 程式人生 > 其它 >Go 語言陷阱 - 陣列和切片

Go 語言陷阱 - 陣列和切片

https://geektutu.com/post/hpg-gotchas-array-slice.html

語言陷阱系列文章連結:

原始碼/資料集已上傳到Github - high-performance-go

1 第一個陷阱

1.1 下面程式的輸出是

1
2
3
4
5
6
7
8
9
func foo(a [2]int) {
a[0] = 200
}

func main() {
a := [2]int{1, 2}
foo(a)
fmt.Println(a)
}

1.2 答案

正確的輸出是[1 2],陣列a沒有發生改變。

  • 在 Go 語言中,陣列是一種值型別,而且不同長度的陣列屬於不同的型別。例如[2]int
    [20]int屬於不同的型別。
  • 當值型別作為引數傳遞時,引數是該值的一個拷貝,因此更改拷貝的值並不會影響原值。

我們在切片(slice)效能及陷阱這篇文章中也提到了,為了避免陣列的拷貝,提高效能,建議傳遞陣列的指標作為引數,或者使用切片代替陣列。

1.3 更多

如果將上述程式替換為:

1
2
3
4
5
6
7
8
9
func foo(a *[2]int) {
(*a)[0] = 200
}

func main() {
a := [2]int{1, 2}
foo(&a)
fmt.Println(a)
}

1
2
3
4
5
6
7
8
9
func foo(a []int) {
a[0] = 200
}

func main() {
a := []int{1, 2}
foo(a)
fmt.Println(a)
}

輸出將會變成[200 2]

切片(slice)效能及陷阱這篇文章中,我們也提到了切片由三個值構成:

  • *ptr指向底層陣列的指標
  • len長度
  • cap容量

因此,將切片作為引數時,拷貝了一個新切片,即拷貝了構成切片的三個值,包括底層陣列的指標。對切片中某個元素的修改,實際上是修改了底層陣列中的值,因此原切片也發生了改變。

2 第二個陷阱

2.1 下面程式的輸出是

1
2
3
4
5
6
7
8
9
10
func foo(a []int) {
a = append(a, 1, 2, 3, 4, 5, 6, 7, 8)
a[0] = 200
}

func main() {
a := []int{1, 2}
foo(a)
fmt.Println(a)
}

2.2 答案

輸出仍是[1 2],切片a沒有發生改變。

傳參時拷貝了新的切片,因此當新切片的長度發生改變時,原切片並不會發生改變。而且在函式foo中,新切片a增加了 8 個元素,原切片對應的底層陣列不夠放置這 8 個元素,因此申請了新的空間來放置擴充後的底層陣列。這個時候新切片和原切片指向的底層陣列就不是同一個了。因此,對新切片第 0 個元素的修改,並不會影響原切片的第 0 個元素。

如果如果希望foo函式的操作能夠影響原切片呢?

兩種方式:

  • 設定返回值,將新切片返回並賦值給main函式中的變數a
  • 切片也使用指標方式傳參。
1
2
3
4
5
6
7
8
9
10
11
func foo(a []int) []int {
a = append(a, 1, 2, 3, 4, 5, 6, 7, 8)
a[0] = 200
return a
}

func main() {
a := []int{1, 2}
a = foo(a)
fmt.Println(a)
}

1
2
3
4
5
6
7
8
9
10
func foo(a *[]int) {
*a = append(*a, 1, 2, 3, 4, 5, 6, 7, 8)
(*a)[0] = 200
}

func main() {
a := []int{1, 2}
foo(&a)
fmt.Println(a)
}

上述兩個程式的輸出均為:

1
[200 2 1 2 3 4 5 6 7 8]

從可讀性上來說,更推薦第一種方式