首頁 > 後端開發 > Golang > 主體

Go:指標與記憶體管理

Patricia Arquette
發布: 2024-11-22 01:51:14
原創
439 人瀏覽過

Go: Pointers & Memory Management

TL;DR:透過範例探索 Go 的記憶體處理,包括指標、堆疊和堆疊分配、逃脫分析和垃圾收集

當我第一次開始學習 Go 時,我對其記憶體管理方法很感興趣,尤其是在指標方面。 Go 以一種既高效又安全的方式處理內存,但如果你不深入了解它的本質,它可能有點像一個黑盒子。我想分享一些關於 Go 如何使用指標、堆疊和堆疊管理記憶體以及逃逸分析和垃圾收集等概念的見解。在此過程中,我們將查看在實踐中說明這些想法的程式碼範例。

了解堆疊和堆疊內存

在深入研究 Go 中的指標之前,了解堆疊和堆疊的工作原理會很有幫助。這是兩個可以儲存變數的記憶體區域,每個區域都有自己的特點。

  • 堆疊:這是一個以後進先出方式操作的記憶體區域。它快速且高效,用於儲存具有短期作用域的變量,例如函數內的局部變數。
  • :這是一個更大的記憶體池,用於儲存需要超出函數範圍的變量,例如從函數返回並在其他地方使用的資料。

在 Go 中,編譯器會根據變數的使用方式決定是在堆疊還是堆上分配變數。這個決策過程稱為逃脫分析,我們稍後將更詳細地探討。

按值傳遞:預設行為

在 Go 中,當您將整數、字串或布林值等變數傳遞給函數時,它們自然是按值傳遞的。這意味著創建了變數的副本,並且該函數可以使用該副本。這意味著,對函數內部變數所做的任何更改都不會影響其作用域之外的變數。

這是一個簡單的例子:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
登入後複製
登入後複製
登入後複製
登入後複製

輸出:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
登入後複製
登入後複製
登入後複製
登入後複製

在此程式碼中:

  • increment() 函數接收 n 的副本。
  • main()中的n和increment()中的num的位址不同。
  • 修改increment()中的num不會影響main()中的n。

重點:按值傳遞是安全且直接的,但對於大型資料結構,複製可能會變得低效。

指針簡介:透過引用傳遞

要修改函數內的原始變量,可以傳遞一個指標給它。指標保存變數的記憶體位址,允許函數存取和修改原始資料。

以下是如何使用指標:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

登入後複製
登入後複製
登入後複製
登入後複製

輸出:

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
登入後複製
登入後複製

在此範例中:

  • 我們將 n 的位址傳遞給incrementPointer()。
  • main() 和incrementPointer() 都引用相同的記憶體位址。
  • 修改incrementPointer()中的num會影響main()中的n。

重點:使用指標允許函數修改原始變量,但它引入了有關記憶體分配的注意事項。

使用指標分配記憶體

當你建立一個指向變數的指標時,Go 需要確保變數與指標一樣存活。這通常意味著在 上分配變量,而不是 堆疊

考慮這個函數:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
登入後複製
登入後複製
登入後複製
登入後複製

這裡,num 是 createPointer() 中的局部變數。如果 num 儲存在堆疊中,一旦函數返回,它就會被清除,留下一個懸空指標。為了防止這種情況,Go 在堆上分配 num ,以便在 createPointer() 退出後它仍然有效。

懸掛指針

當指標引用已釋放的記憶體時,就會出現懸空指標

Go 透過其垃圾收集器防止懸空指針,確保內存在仍被引用時不會被釋放。然而,在某些情況下,持有指標的時間超過必要的時間可能會導致記憶體使用量增加或記憶體洩漏。

逃逸分析:決定堆疊與堆疊分配

逃逸分析決定變數是否需要存在於其函數範圍之外。如果一個變數被傳回、儲存在指標中或被 goroutine 捕獲,它就會逃逸並分配在堆上。但是,即使變數沒有轉義,編譯器也可能出於其他原因(例如最佳化決策或堆疊大小限制)將其分配在堆疊上。

變數轉義範例:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
登入後複製
登入後複製
登入後複製
登入後複製

在此程式碼中:

  • createSlice() 中的切片資料會轉義,因為它在 main() 中傳回並使用。
  • 切片的底層陣列分配在

使用 go build -gcflags '-m' 來了解轉義分析

你可以透過使用 -gcflags '-m' 選項來查看 Go 編譯器的決定:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

登入後複製
登入後複製
登入後複製
登入後複製

這將輸出指示變數是否逃逸到堆疊的訊息。

Go 中的垃圾收集

Go 使用垃圾收集器來管理堆上的記憶體分配和釋放。它會自動釋放不再引用的內存,有助於防止內存洩漏。

範例:

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
登入後複製
登入後複製

在此程式碼中:

  • 我們建立一個包含 1,000,000 個節點的鍊錶。
  • 每個 Node 都分配在堆上,因為它逃脫了 createLinkedList() 的範圍。
  • 當不再需要清單時,垃圾收集器會釋放記憶體。

重點:Go 的垃圾收集器簡化了記憶體管理,但會帶來開銷。

指針的潛在陷阱

雖然指針很強大,但如果使用不小心,它們可能會導致問題。

懸空指針(續)

儘管 Go 的垃圾收集器有助於防止懸空指針,但如果持有指針的時間超過必要的時間,仍然可能會遇到問題。

範例:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
登入後複製
登入後複製
登入後複製
登入後複製

在此程式碼中:

  • data 是分配在堆疊上的一個大切片。
  • 保留對它的引用 ([]int),我們可以防止垃圾收集器釋放記憶體。
  • 如果管理不當,可能會導致記憶體使用量增加。

並發問題 - 與指標的資料爭用

以下是直接涉及指標的範例:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
登入後複製
登入後複製
登入後複製
登入後複製

為什麼此程式碼失敗:

  • 多個 goroutine 取消引用並遞增指標 counterPtr,無需任何同步。
  • 這會導致資料競爭,因為多個 goroutine 在沒有同步的情況下同時存取和修改相同記憶體位置。 *counterPtr 操作涉及多個步驟(讀取、遞增、寫入)且不是執行緒安全的。

修正資料爭用:

我們可以透過加入互斥體同步來解決這個問題:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

登入後複製
登入後複製
登入後複製
登入後複製

此修復的工作原理:

  • mu.Lock() 和 mu.Unlock() 確保一次只有一個 goroutine 存取和修改指標。
  • 這可以防止競爭條件並確保計數器的最終值是正確的。

Go 的語言規範怎麼說?

值得注意的是,Go 的語言規格並沒有直接規定變數是分配在堆疊上還是堆上。這些是運行時和編譯器實作細節,允許根據 Go 版本或實現的不同而變化的靈活性和最佳化。

這表示:

  • 不同版本的 Go 之間管理記憶體的方式可能會有所不同。
  • 您不應依賴在特定記憶體區域中分配的變數。
  • 專注於編寫清晰正確的程式碼,而不是試圖控制記憶體分配。

範例:

即使您希望在堆疊上分配變量,編譯器也可能會根據其分析決定將其移至堆疊。

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
登入後複製
登入後複製
登入後複製
登入後複製

要點:由於記憶體分配細節是一種內部實現,而不是 Go 語言規範的一部分,因此這些資訊只是一般準則,而不是以後可能會更改的固定規則。

平衡效能和記憶體使用

在決定按值傳遞還是按指標傳遞時,我們必須考慮資料的大小和效能影響。

以數值傳遞大型結構:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
登入後複製
登入後複製
登入後複製
登入後複製

透過指標傳遞大型結構:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

登入後複製
登入後複製
登入後複製
登入後複製

注意事項:

  • 按值傳遞安全且簡單,但對於大型資料結構可能效率低。
  • 透過指標傳遞可以避免複製,但需要仔細處理以避免併發問題。

從現場經驗來看:

在早期職業生涯中,我記得有一次我正在優化處理大量資料的 Go 應用程式。最初,我按值傳遞大型結構,假設這會簡化程式碼的推理。然而,我碰巧注意到記憶體使用率相對較高,而且垃圾收集經常暫停。

在與我的學長結對程式設計中使用 Go 的 pprof 工具對應用程式進行分析後,我們發現複製大型結構是一個瓶頸。我們重構了程式碼以傳遞指標而不是值。這顯著減少了記憶體使用並提高了效能。

但這項改變並非沒有挑戰。我們必須確保我們的程式碼是線程安全的,因為多個 goroutine 現在正在存取共享資料。我們使用互斥鎖實現了同步,並仔細檢查了程式碼中潛在的競爭條件。

經驗教訓:儘早了解 Go 如何處理記憶體分配可以幫助您編寫更有效率的程式碼,因為平衡效能提升與程式碼安全性和可維護性至關重要。

最後的想法

Go 的記憶體管理方法(就像其他地方的做法一樣)在效能和簡單性之間取得了平衡。透過抽像出許多低階細節,它使開發人員能夠專注於建立強大的應用程序,而不必陷入手動記憶體管理的困境。

要記住的重點:

  • 以數值傳遞很簡單,但對於大型資料結構可能效率低。
  • 使用指標可以提高效能,但需要仔細處理以避免資料爭用等問題。
  • 逃逸分析確定變數是分配在堆疊上還是堆上,但這是內部細節。
  • 垃圾收集有助於防止記憶體洩漏,但可能會帶來開銷。
  • 並發涉及共享資料時需要同步。

透過牢記這些概念並使用 Go 的工具來分析和分析您的程式碼,您可以編寫高效且安全的應用程式。


我希望對 Go 使用指標進行記憶體管理的探索會有所幫助。無論您是剛開始使用 Go 還是希望加深理解,嘗試程式碼並觀察編譯器和運行時的行為都是一種很好的學習方式。

請隨意分享您的經驗或您可能遇到的任何問題 - 我總是熱衷於討論、學習和撰寫更多有關 Go 的內容!

獎勵內容 - 直接指針支持

你知道嗎?可以為某些資料類型直接建立指針,但對於某些資料類型則不能。這張短桌子涵蓋了它們。


Type Supports Direct Pointer Creation? Example
Structs ✅ Yes p := &Person{Name: "Alice", Age: 30}
Arrays ✅ Yes arrPtr := &[3]int{1, 2, 3}
Slices ❌ No (indirect via variable) slice := []int{1, 2, 3}; slicePtr := &slice
Maps ❌ No (indirect via variable) m := map[string]int{}; mPtr := &m
Channels ❌ No (indirect via variable) ch := make(chan int); chPtr := &ch
Basic Types ❌ No (requires a variable) val := 42; p := &val
time.Time (Struct) ✅ Yes t := &time.Time{}
Custom Structs ✅ Yes point := &Point{X: 1, Y: 2}
Interface Types ✅ Yes (but rarely needed) var iface interface{} = "hello"; ifacePtr := &iface
time.Duration (Alias of int64) ❌ No duration := time.Duration(5); p := &duration
類型 支援直接建立指標嗎? 範例 標題> 結構 ✅ 是的 p := &Person{姓名:“Alice”,年齡:30} 數組 ✅ 是的 arrPtr := &[3]int{1, 2, 3} 切片 ❌否(透過變數間接) 切片 := []int{1, 2, 3}; slicePtr := &切片 地圖 ❌否(透過變數間接) m := map[string]int{}; mPtr := &m 頻道 ❌否(透過變數間接) ch := make(chan int); chPtr := &ch 基本類型 ❌否(需要變數) val := 42; p := &val time.Time(結構) ✅ 是的 t := &time.Time{} 自訂結構 ✅ 是的 點 := &Point{X: 1, Y: 2} 介面類型 ✅ 是(但很少需要) var iface 介面{} = "你好"; ifacePtr := &iface time.Duration(int64 的別名) ❌沒有 持續時間 := 時間.持續時間(5); p := &持續時間 表>

如果您喜歡這個,請在評論中告訴我;我會嘗試在以後的文章中添加此類獎勵內容。

感謝您的閱讀!想了解更多內容,請考慮關注。

願程式碼與你同在:)

我的社群連結:LinkedIn | GitHub | ? (原推特)|子棧 |開發者 |哈希節點

以上是Go:指標與記憶體管理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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