👨‍💻簡介

當我們在宣告變數時,電腦會為該變數在記憶體中分配一個位置,然後將這個變數值儲存在這個位置上,需要讀取或修改這個變數值時,電腦是透過記憶體位置來存取這個值。 今天來簡單介紹一下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