切片有哪些注意事項是一定要知道的呢
1. 引言
在之前我寫了一篇 切片比數組好用在哪 的文章,仔細介紹了切片相比于數組的優點。但切片事實上也隱藏著一些潛在的陷阱和需要注意的細節,了解和掌握切片的使用注意事項,可以避免意外的程序行為。本文將深入探討Go語言切片常見的注意事項,從而能夠更好得使用切片。
2. 注意事項
2.1 注意一個數組可以同時被多個切片引用
當創建一個切片時,它實際上是對一個底層數組的引用。這意味著對切片的修改會直接影響到底層數組以及其他引用該數組的切片。這種引用關系可能導致一些意想不到的結果,下面是一個示例代碼來說明這個問題:
package main
import "fmt"
func main() {
array := [5]int{1, 2, 3, 4, 5}
firstSlice := array[1:4] // 創建一個切片,引用了底層數組的索引1到3的元素
secondSlice := array[1:3]
fmt.Println("Original array:", firstSlice) // 輸出第一個切片 [2 3 4]
fmt.Println("Original slice:", secondSlice) // 輸出第二個切片 [2 3]
// 修改切片的第一個元素
firstSlice[0] = 10
fmt.Println("Modified array:", firstSlice) // 輸出第一個切片 [10 3 4]
fmt.Println("Modified slice:", secondSlice) // 輸出第二個切片 [10 3]
}
在上述代碼中,我們創建了一個長度為5的數組array和兩個引用該數組的切片firstSlice和secondSlice。當我們修改第一個切片的第一個元素為10時,底層數組的對應位置的元素也被修改了。這里導致了數組和其他引用該數組的切片的內容也會受到影響。
如果我們有多個切片同時引用了同一個底層數組,同時我們并不想由于對某個切片的修改,影響到另外一個切片的數據,此時我們可以新創建一個切片,使用內置的copy函數來復制原切片元素的值。示例代碼如下:
package main
import "fmt"
func main() {
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4]
// 復制切片創建一個獨立的底層數組
newSlice := make([]int, len(slice))
copy(newSlice, slice)
fmt.Println("Original array:", array) // 輸出原始數組 [1 2 3 4 5]
fmt.Println("Original slice:", slice) // 輸出初始切片 [2 3 4]
fmt.Println("New slice:", newSlice) // 輸出新創建的切片 [2 3 4]
// 修改newSlice的第一個元素
newSlice[0] = 10
fmt.Println("Modified array:", array)// 輸出修改后的數組 [1 2 3 4 5]
fmt.Println("Original slice:", slice)// 輸出初始切片 [2 3 4]
fmt.Println("New slice:", newSlice)// 輸出修改后的切片 [10 3 4]
}
通過創建了一個新的切片newSlice,它擁有獨立的底層數組,同時使用copy函數復制原切片的值,我們現在修改newSlice不會影響原始數組或原始切片。
2.2 注意自動擴容可能帶來的性能問題
在Go語言中,切片的容量是指底層數組的大小,而長度是切片當前包含的元素數量。當切片的長度超過容量時,Go語言會自動擴容切片。擴容操作涉及到重新分配底層數組,并將原有數據復制到新的數組中。下面先通過一個示例代碼,演示切片的自動擴容機制:
package main
import "fmt"
func main() {
slice := make([]int, 3, 5) // 創建一個初始長度為3,容量為5的切片
fmt.Println("Initial slice:", slice) // 輸出初始切片 [0 0 0]
fmt.Println("Length:", len(slice)) // 輸出切片長度 3
fmt.Println("Capacity:", cap(slice)) // 輸出切片容量 5
slice = append(slice, 1, 2, 3) // 添加3個元素到切片,長度超過容量
fmt.Println("After appending:", slice) // 輸出擴容后的切片 [0 0 0 1 2 3]
fmt.Println("Length:", len(slice)) // 輸出切片長度 6
fmt.Println("Capacity:", cap(slice)) // 輸出切片容量 10
}
在上述代碼中,我們使用make函數創建了一個初始長度為3,容量為5的切片slice。然后,我們通過append函數添加了3個元素到切片,導致切片的長度超過了容量。此時,Go語言會自動擴容切片,創建一個新的底層數組,并將原有數據復制到新的數組中。最終,切片的長度變為6,容量變為10。
但是切片的自動擴容機制,其實是存在性能開銷的,需要創建一個新的數組,同時將數據全部拷貝到新數組中,切片再引用新的數組。下面先通過基準測試,展示沒有設置初始容量和設置了初始容量兩種情況下的性能差距:
package main
import (
"fmt"
"testing"
)
func BenchmarkSliceAppendNoCapacity(b *testing.B) {
for i := 0; i < b.N; i++ {
var slice []int
for j := 0; j < 1000; j++ {
slice = append(slice, j)
}
}
}
func BenchmarkSliceAppendWithCapacity(b *testing.B) {
for i := 0; i < b.N; i++ {
slice := make([]int, 0, 1000)
for j := 0; j < 1000; j++ {
slice = append(slice, j)
}
}
}
在上述代碼中,我們定義了兩個基準測試函數:BenchmarkSliceAppendNoCapacity和BenchmarkSliceAppendWithCapacity。其中,BenchmarkSliceAppendNoCapacity測試了在沒有設置初始容量的情況下,循環追加元素到切片的性能;BenchmarkSliceAppendWithCapacity測試了在設置了初始容量的情況下,循環追加元素到切片的性能。基準測試結果如下:
BenchmarkSliceAppendNoCapacity-4 280983 4153 ns/op 25208 B/op 12 allocs/op
BenchmarkSliceAppendWithCapacity-4 1621177 712.2 ns/op 0 B/op 0 allocs/op
其中ns/op 表示每次操作的平均執行時間,即函數執行的耗時。B/op 表示每次操作的平均內存分配量,即每次操作分配的內存大小。allocs/op 表示每次操作的平均內存分配次數。
可以看到,在設置了初始容量的情況下,性能要明顯優于沒有設置初始容量的情況。循環追加1000個元素到切片時,設置了初始容量的情況下平均每次操作耗時約為712.2納秒,而沒有設置初始容量的情況下平均每次操作耗時約為4153 納秒。這是因為設置了初始容量避免了頻繁的擴容操作,提高了性能。
所以,雖然切片的自動擴容好用,但是其也是存在代價的。更好得使用切片,應該避免頻繁的擴容操作,這里可以在創建切片時預估所需的容量,并提前指定切片的容量,這樣可以減少擴容次數,提高性能。需要注意的是,如果你不知道切片需要多大的容量,可以使用適當的初始容量,然后根據需要動態擴容。
2.3 注意切片參數修改原始數據的陷阱
在Go語言中,切片是引用類型。當將切片作為參數傳遞給函數時,實際上是傳遞了底層數組的引用。這意味著在函數內部修改切片的元素會影響到原始切片。下面是一個示例代碼來說明這個問題:
package main
import "fmt"
func modifySlice(slice []int) {
slice[0] = 10
fmt.Println("Modified slice inside function:", slice)
}
func main() {
originalSlice := []int{1, 2, 3}
fmt.Println("Original slice:", originalSlice)
modifySlice(originalSlice)
fmt.Println("Original slice after function call:", originalSlice)
}
在上述代碼中,我們定義了一個modifySlice函數,它接收一個切片作為參數,并在函數內部修改了切片的第一個元素,并追加了一個新元素。然后,在main函數中,我們創建了一個初始切片originalSlice,并將其作為參數傳遞給modifySlice函數。當我們運行代碼時,輸出如下:
Original slice: [1 2 3]
Modified slice inside function: [10 2 3]
Original slice after function call: [10 2 3]
可以看到,在modifySlice函數內部,我們修改了切片的第一個元素并追加了一個新元素。這導致了函數內部切片的變化。然而,當函數返回后,原始切片originalSlice數據也受到影響。
如果我們希望函數內部的修改不影響原始切片,可以通過復制切片來解決。修改示例代碼如下:
package main
import "fmt"
func modifySlice(slice []int) {
newSlice := make([]int, len(slice))
copy(newSlice, slice)
newSlice[0] = 10
fmt.Println("Modified slice inside function:", newSlice)
}
func main() {
originalSlice := []int{1, 2, 3}
fmt.Println("Original slice:", originalSlice)
modifySlice(originalSlice)
fmt.Println("Original slice after function call:", originalSlice)
}
通過使用make函數創建一個新的切片newSlice,并使用copy函數將原始切片復制到新切片中,我們確保了函數內部操作的是新切片的副本。這樣,在修改新切片時不會影響原始切片的值。當我們運行修改后的代碼時,輸出如下:
Original slice: [1 2 3]
Modified slice inside function: [10 2 3]
Original slice after function call: [1 2 3]
可以看到,原始切片保持了不變,函數內部的修改只影響了復制的切片。這樣我們可以避免在函數間傳遞切片時對原始切片造成意外修改。
3. 總結
本文深入探討了Go語言切片的一些注意事項,旨在幫助讀者更好地使用切片。
首先,切片是對底層數組的引用。修改切片的元素會直接影響到底層數組以及其他引用該數組的切片。如果需要避免修改一個切片影響其他切片或底層數組,可以使用copy函數創建一個獨立的底層數組。
其次,切片的自動擴容可能帶來性能問題。當切片的長度超過容量時,Go語言會自動擴容切片,需要重新分配底層數組并復制數據。為了避免頻繁的擴容操作,可以在創建切片時預估所需的容量,并提前指定切片的容量。
最后,需要注意切片作為參數傳遞給函數時,函數內部的修改會影響到原始切片。如果希望函數內部的修改不影響原始切片,可以通過復制切片來解決。
了解和掌握這些切片的注意事項和技巧,可以避免意外的程序行為。

浙公網安備 33010602011771號