2023-09-08|閱讀時間 ‧ 約 9 分鐘

淺談Go中的Pointer和記憶體管理


raw-image

👨‍💻簡介

當我們在宣告變數時,電腦會為該變數在記憶體中分配一個位置,然後將這個變數值儲存在這個位置上,需要讀取或修改這個變數值時,電腦是透過記憶體位置來存取這個值。 今天來簡單介紹一下go的Pointer,他的特性以及常見用法。

什麼是Pointer?

Pointer是一種資料類型,用來儲存變數的記憶體地址。在Go中,我們可以通過使用 * 符號來宣告和操作Pointer。這允許我們直接訪問和修改變數的內容,而不僅僅是讀取或複製它們的值。

Pointer的特性、限制與常見用法

特性

  • Pointer的值和地址

每個變數都有一個記憶體地址,我們可以使用Pointer變數來儲存這個地址。讓我們看一個範例:

package main

import "fmt"
func main() {
x := 42
var p *int // 宣告一個整數Pointer
p = &x // 將p指向x的地址
fmt.Println("x =", x)
fmt.Println("p =", p)
}

在這個例子中,我們創建了一個整數變數 x,並宣告了一個整數Pointer p,然後將 p 設為 x 的地址。現在,p 裡面存放的就是 x 的地址。

  • Pointer的初始化

Go中的Pointer可以通過 new 函數來初始化,這將為指定的類型分配記憶體並返回其地址。範例:

package main

import "fmt"
func main() {
var p *int
p = new(int) // 初始化一個整數Pointer
*p = 123 // 將Pointer所指向的記憶體設置為123
fmt.Println("*p =", *p)
}

可以看到,我們創建了一個整數Pointer p,並使用 new(int) 初始化它,然後將 p 所指向的記憶體設置為 123

  • Pointer的解引用

通過Pointer,我們可以訪問和修改變數的值,這稱為Pointer的解引用。

package main

import "fmt"
func main() {
x := 42
p := &x // 將p設為x的地址,使用&
fmt.Println("x =", x)
fmt.Println("&x =", &x)
fmt.Println("*p =", *p) // 解引用Pointer,獲得x的值,使用*
*p = 99 // 通過Pointer修改x的值
fmt.Println("x =", x) // x的值已被修改
fmt.Println("&x =", &x)
fmt.Println("*p =", *p)
}

在這個例子中,我們使用Pointer p 解引用,並修改了變數 x 的值。也可以觀察到修改值並不會改變記憶體位置。

限制

  1. 空Pointer風險:未初始化的Pointer可能為nil,試圖解引用nil Pointer會導致運行時錯誤(panic)。
  2. 記憶體管理:Go中的垃圾回收器會管理記憶體,但Pointer仍然需要謹慎使用,以避免記憶體錯誤。
  3. 競爭條件:多線程環境中,共享的Pointer可能導致競爭條件,需要使用互斥鎖等技術來保護共享資源。

常見用法

  • 節省記憶體

當需要處理大型資料結構時,使用Pointer可以節省記憶體,因為它們只儲存變數的地址,而不是整個資料。

package main

import (
"fmt"
"unsafe"
)
// 定義一個名為 Person 的結構(struct)
type Person struct {
Name string // 名稱
Age int // 年齡
}
func main() {
// 創建一個 Person 變數,名稱為 Alice,年齡為 30 歲
alice := Person{Name: "Alice", Age: 30}
// 計算 Person 變數 alice 的大小
personSize := unsafe.Sizeof(alice)
// 創建一個指向 Person 變數的Pointer,名稱為 Bob,年齡為 24 歲
bob := &Person{Name: "Bob", Age: 24}
// 計算指向 Person 變數 bob 的Pointer的大小
pointerSize := unsafe.Sizeof(bob)
fmt.Printf("alice -> Person 變數的大小: %d 個位元組\n", personSize)
fmt.Printf("bob -> 指向 Person 變數的Pointer的大小: %d 個位元組\n", pointerSize)
fmt.Println(&alice) // 列印 alice 變數的記憶體位置
fmt.Println(&bob) // 列印 bob 變數(Pointer)的記憶體位置
}

在上面的範例中,我們透過兩種方式創建person,但可以觀察到使用Pointer的方式創建,使用的大小較小,達到節省記憶體的作用。

  • 改變資料的原始值

想要在函數中修改變數的值,而不只是副本時,Pointer變得非常有用。

package main

import "fmt"
func modifyValue(x *int) {
*x = 100
}
func main() {
y := 10
modifyValue(&y) // 通過Pointer修改y的值
fmt.Println("y =", y)
}
  • 效能優勢

在某些情況下,使用Pointer可以提高性能,因為它們允許直接訪問和修改記憶體,而不需要額外的記憶體複製操作。

package main

import (
"fmt"
"time"
)
const N = 1000000 // 陣列大小
// 使用值傳遞的函數,不使用指標
func sumValues(arr [N]int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}
// 使用指標傳遞的函數,避免陣列複製
func sumPointers(arr *[N]int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}
func main() {
// 創建一個包含大量資料的整數陣列
var arr [N]int
for i := 0; i < N; i++ {
arr[i] = i
}
// 測試不使用指標的情況,複製陣列
startTime := time.Now()
result1 := sumValues(arr)
duration1 := time.Since(startTime)
// 測試使用指標的情況,避免複製陣列
startTime = time.Now()
result2 := sumPointers(&arr)
duration2 := time.Since(startTime)
fmt.Printf("不使用指標的結果:%d,執行時間:%v\n", result1, duration1)
fmt.Printf("使用指標的結果:%d,執行時間:%v\n", result2, duration2)
}

可以看到,我們創建了一個包含大量資料的整數陣列,並寫了兩個函數來計算陣列中所有元素的總和。一個函數 sumValues 使用值傳遞陣列,另一個函數 sumPointers 使用指標傳遞陣列。

在 main 函數中,測試了這兩個函數的效能。結果顯示,使用指標傳遞陣列可以明顯提高效能,因為它少了陣列的複製,節省了大量的時間和記憶體。

小結

Pointer是程式語言中的重要概念,它允許我們直接訪問和修改變數的內容,並在許多情況下提供性能優勢和節省記憶體的機會。但還是要小心使用Pointer,確保程式碼的穩定性和安全性。

📚Reference

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.