深入理解Go語言切片:共享內存與append()
陷阱
大家好!歡迎回到我的博客。 ?如果您在這裡,您可能剛接觸Golang,或者您是經驗豐富的開發者,想深入了解切片的內部工作原理。那麼,讓我們開始吧!
Go語言因其簡潔性和高效性而備受讚譽——正如人們常說的那樣,“Go語言就是能完成工作”。對於我們這些來自C、C 或Java等語言的開發者來說,Go語言簡潔明了的語法和易用性令人耳目一新。然而,即使在Go語言中,某些特性也可能讓開發者感到困惑,尤其是在處理切片和子切片時。讓我們揭開這些細微之處,更好地理解如何避免append()
和切片共享內存的常見陷阱。
Go語言中的切片是什麼?
通常,當您需要一種數據結構來存儲一系列值時,切片是Go語言中的首選。它們的靈活性來自於這樣一個事實:它們的長度不是其類型的一部分。此特性克服了數組的限制,使我們能夠創建一個可以處理任何大小切片的單個函數,並使切片能夠根據需要增長或擴展。
雖然切片與數組有一些相似之處,例如都是可索引的並且具有長度,但它們在數據管理方式上有所不同。切片充當對底層數組的引用,該數組實際上存儲切片的數據。從本質上講,切片提供對該數組的某些或所有元素的視圖。因此,當您創建一個切片時,Go會自動處理創建保存切片元素/數據的底層數組。
切片的共享內存
數組是連續的內存塊,但使切片有趣的是它們如何引用此內存。讓我們分解切片的結構:
type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }
當您創建一個切片時,它包含三個組件:
len
切片的長度(它包含的元素數量)cap
容量(在需要增長之前它可以包含的元素數量)這就是事情變得有趣的地方。如果您有多個派生自同一數組的切片,則通過一個切片進行的更改將體現在其他切片中,因為它們共享相同的底層數組。
讓我們看下面的例子:
package main import "fmt" func main() { // 创建一个具有初始值的切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片——两个切片共享相同的底层数组! subslice := original[1:3] fmt.Println("未修改的子切片:", subslice) // 输出 => 未修改的子切片: [2 3] // 修改子切片 subslice[0] = 42 fmt.Println("原始切片:", original) // 输出 => 原始切片: [1 42 3 4 5] fmt.Println("修改后的子切片:", subslice) // 输出 => 修改后的子切片: [42 3] }
理解切片容量
在我們進一步深入之前,讓我們嘗試理解切片容量cap()
。當您從現有的Go語言切片中獲取子切片時,新子切片的容量由原始切片從子切片開始位置的剩餘容量決定。讓我們稍微分解一下:
當您從數組創建切片時,切片的長度是它最初包含的元素數量,而它的容量是它在需要增長之前可以包含的元素總數。
當您從現有切片中獲取子切片時:
讓我們看一個詳細的例子:
type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }
subslice := original[1:4]
時,它指向從索引 1 到 3 的元素 (2, 3, 4)。 subslice
的長度是 4 - 1 = 3。 subslice
的容量是 5 - 1 = 4,因為它從索引 1 開始,並包含到原始切片末尾的元素。 append()
陷阱!
這就是開發者經常被困住的地方。 Go語言中的append()
函數在處理子切片時可能會導致意外行為。
子切片的容量包括不屬於其長度但位於原始切片容量範圍內的元素。這意味著如果子切片增長,它可以訪問或修改這些元素。
讓我們考慮這個例子:
package main import "fmt" func main() { // 创建一个具有初始值的切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片——两个切片共享相同的底层数组! subslice := original[1:3] fmt.Println("未修改的子切片:", subslice) // 输出 => 未修改的子切片: [2 3] // 修改子切片 subslice[0] = 42 fmt.Println("原始切片:", original) // 输出 => 原始切片: [1 42 3 4 5] fmt.Println("修改后的子切片:", subslice) // 输出 => 修改后的子切片: [42 3] }
subslice
最初指向 2, 3,容量為 4(它可以增長到原始切片的末尾)。 subslice
追加 60, 70 時,它使用原始切片的剩餘容量。 original
和subslice
都反映了這些更改,因為它們共享相同的底層數組。 驚訝嗎? append()
操作修改了原始切片,因為底層數組中有足夠的容量。但是,如果我們超過容量或追加的元素超過容量允許的範圍,Go將為子切片分配一個新的數組,從而打破與原始切片的共享:
func main() { // 原始切片 original := []int{1, 2, 3, 4, 5} // 创建一个子切片 subslice := original[1:4] // 指向元素 2, 3, 4 fmt.Println("子切片:", subslice) // 输出 => 子切片: [2 3 4] fmt.Println("子切片的长度:", len(subslice)) // 输出 => 子切片的长度: 3 fmt.Println("子切片的容量:", cap(subslice)) // 输出 => 子切片的容量: 4 }
在這種情況下,append()
創建了一個新的底層數組,因為原始容量已超過。
避免陷阱的最佳實踐
func main() { original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // 指向元素 2, 3 fmt.Println("追加前原始切片:", original) // 输出 => [1 2 3 4 5] fmt.Println("追加前子切片:", subslice) // 输出 => [2 3] fmt.Println("子切片的容量:", cap(subslice)) // 输出 => 4 // 在容量范围内追加到子切片 subslice = append(subslice, 60, 70) // 追加到子切片后打印 fmt.Println("追加后原始切片:", original) // 输出 => [1 2 3 60 70] fmt.Println("追加后子切片:", subslice) // 输出 => [2 3 60 70] }
主要優點是:
i. make([]int, len(subslice))
創建一個具有其自身獨立底層數組的新切片。這至關重要——它不僅僅是一個新的切片頭,而是在內存中一個全新的數組。
ii. copy()
然後只傳輸值,而不是內存引用。這就像複印一份文件而不是共享原始文件。
func main() { original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // 指向元素 2, 3 // 追加超出子切片容量的元素 subslice = append(subslice, 60, 70, 80) fmt.Println("大容量追加后原始切片:", original) // 输出 => [1 2 3 4 5] fmt.Println("大容量追加后子切片:", subslice) // 输出 => [2 3 60 70 80] }
// 假设我们从这里开始 original := []int{1, 2, 3, 4, 5} subslice := original[1:3] // subslice 指向 original 的底层数组 // 这是我们的解决方案: newSlice := make([]int, len(subslice)) // 步骤 1:创建新的底层数组 copy(newSlice, subslice) // 步骤 2:复制值
主要優點是:
i. 數據保護:原始數據保持不變,防止意外副作用
ii. 可預測的行為:函數對輸入沒有隱藏的影響
iii. 並發安全:在處理過程中可以在其他 goroutine 中安全地使用原始數據
記住:
append()
是否創建新的底層數組取決於容量type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片中的元素数量 cap int // 底层数组的容量 }
祝您編碼愉快。記住,能力越大,責任越大,尤其是在共享內存方面! ?
恭喜您閱讀完本文。
您覺得這篇資源有幫助嗎?您有問題嗎?或者您發現了錯誤或錯別字?請在評論中留下您的反饋。
不要忘記與可能從中受益的其他人分享此資源。關注我以獲取更多信息。
以上是進行切片和子鏈條:了解共享內存並避免```append''的詳細內容。更多資訊請關注PHP中文網其他相關文章!