首頁 > 後端開發 > Golang > 進行切片和子鏈條:了解共享內存並避免```append''

進行切片和子鏈條:了解共享內存並避免```append''

Barbara Streisand
發布: 2025-01-29 00:21:10
原創
918 人瀏覽過

Go Slices and Subslices: Understanding Shared Memory and Avoiding `append()` Pitfalls

深入理解Go語言切片:共享內存與append()陷阱

大家好!歡迎回到我的博客。 ?如果您在這裡,您可能剛接觸Golang,或者您是經驗豐富的開發者,想深入了解切片的內部工作原理。那麼,讓我們開始吧!

Go語言因其簡潔性和高效性而備受讚譽——正如人們常說的那樣,“Go語言就是能完成工作”。對於我們這些來自C、C 或Java等語言的開發者來說,Go語言簡潔明了的語法和易用性令人耳目一新。然而,即使在Go語言中,某些特性也可能讓開發者感到困惑,尤其是在處理切片和子切片時。讓我們揭開這些細微之處,更好地理解如何避免append()和切片共享內存的常見陷阱。

Go語言中的切片是什麼?

通常,當您需要一種數據結構來存儲一系列值時,切片是Go語言中的首選。它們的靈活性來自於這樣一個事實:它們的長度不是其類型的一部分。此特性克服了數組的限制,使我們能夠創建一個可以處理任何大小切片的單個函數,並使切片能夠根據需要增長或擴展。

雖然切片與數組有一些相似之處,例如都是可索引的並且具有長度,但它們在數據管理方式上有所不同。切片充當對底層數組的引用,該數組實際上存儲切片的數據。從本質上講,切片提供對該數組的某些或所有元素的視圖。因此,當您創建一個切片時,Go會自動處理創建保存切片元素/數據的底層數組。

切片的共享內存

數組是連續的內存塊,但使切片有趣的是它們如何引用此內存。讓我們分解切片的結構:

type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int           // 切片中的元素数量
    cap   int           // 底层数组的容量
}
登入後複製
登入後複製
登入後複製

當您創建一個切片時,它包含三個組件:

  1. 指向底層數組的指針
  2. len 切片的長度(它包含的元素數量)
  3. 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           // 底层数组的容量
}
登入後複製
登入後複製
登入後複製
  • 原始切片有 5 個元素,長度和容量均為 5。
  • 當您使用 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 時,它使用原始切片的剩餘容量。
  • originalsubslice都反映了這些更改,因為它們共享相同的底層數組。

驚訝嗎? 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中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板