本文 AI 產出,尚未審核

Golang

單元:指標與記憶體管理

主題:指標的宣告與操作(&*


簡介

在 Go 語言中,**指標(pointer)**是連結變數與其實際記憶體位置的橋樑。透過指標,我們可以在函式之間傳遞大型資料結構而不必複製整個值,亦能直接修改原始資料。這對於提升效能、降低記憶體使用量以及實作如鏈結串列、樹狀結構等資料結構都是必備的基礎。

本單元將聚焦於 指標的宣告、取得地址(&)與解引用(* 兩大操作。即使你是剛踏入 Go 的新手,只要掌握這些概念,就能在日常開發中自信地使用指標,避免常見的陷阱,寫出更安全、可讀性更高的程式碼。


核心概念

1. 什麼是指標?

指標是一種變數,存放的是另一個變數的記憶體位址。其型別寫法為 *T,表示「指向 T 型別的指標」。

  • int → 整數值本身
  • *int → 指向 int 的指標,實際上儲存的是一個記憶體位址(address)

2. 取得變數的位址:&(address‑of)

使用 & 可以取得變數的位址,產生相對應型別的指標。

var x int = 10
p := &x          // p 的型別是 *int,指向 x 的位址

3. 解引用指標:*(dereference)

透過 * 可以取得指標所指向的實際值,或對該值進行寫入。

fmt.Println(*p) // 輸出 10
*p = 20          // 直接修改 x 的值,x 現在變成 20

注意* 在宣告指標型別時與解引用時的語意不同。宣告時寫在型別前(*int),解引用時寫在變數前(*p)。

4. nil 指標

未被初始化的指標預設值是 nil,代表「沒有指向任何有效位址」。在使用指標前務必檢查 nil,避免執行時恐慌(panic)。

var p *int
if p == nil {
    fmt.Println("指標尚未指向任何變數")
}

5. 指標傳遞的特性

Go 的函式參數傳遞是 值傳遞,但若傳入指標,實際上是把指標的「值」(即位址)傳遞過去,讓被呼叫的函式可以直接操作原始資料。

func addOne(v *int) {
    *v = *v + 1   // 直接改變呼叫端的變數
}

程式碼範例

以下示範 5 個常見且實用的指標操作範例,皆附上說明註解。

範例 1:基本的 &*

package main

import "fmt"

func main() {
    a := 42               // 一般變數
    p := &a               // 取得 a 的位址,p 的型別是 *int
    fmt.Printf("a = %d, p = %p\n", a, p)

    *p = 100              // 透過指標修改 a 的值
    fmt.Println("修改後 a =", a) // 輸出 100
}

重點%p 會以十六進位顯示位址。

範例 2:函式接受指標以修改呼叫端變數

package main

import "fmt"

func swap(x, y *int) {
    *x, *y = *y, *x   // 同時交換兩個位址所指向的值
}

func main() {
    a, b := 3, 7
    fmt.Println("交換前:", a, b) // 3 7
    swap(&a, &b)                  // 傳入 a、b 的位址
    fmt.Println("交換後:", a, b) // 7 3
}

範例 3:使用 new 建立指標

new(T) 會分配零值的記憶體,回傳 *T

package main

import "fmt"

func main() {
    p := new(int)   // p 的型別是 *int,指向一個值為 0 的 int
    fmt.Println(*p) // 0
    *p = 55
    fmt.Println(*p) // 55
}

範例 4:結構體指標與方法接收者

package main

import "fmt"

type Counter struct {
    value int
}

// 使用指標接收者,讓方法可以直接修改結構體內部
func (c *Counter) Inc() {
    c.value++
}

func main() {
    c := Counter{value: 10}
    c.Inc()                // 編譯器會自動把 c 取址傳入
    fmt.Println(c.value)  // 11

    // 手動傳遞指標
    p := &c
    p.Inc()
    fmt.Println(p.value)  // 12
}

範例 5:避免 nil 指標導致 panic

package main

import "fmt"

func printLen(s *string) {
    if s == nil {
        fmt.Println("nil pointer, 無法取得長度")
        return
    }
    fmt.Println("長度:", len(*s))
}

func main() {
    var str *string // 預設 nil
    printLen(str)   // 安全處理

    hello := "Hello, Go!"
    str = &hello
    printLen(str)   // 正常輸出長度 11
}

常見陷阱與最佳實踐

陷阱 說明 最佳實踐
忘記檢查 nil 直接解引用 nil 會觸發 runtime panic。 在使用指標前加 if p == nil { … } 或使用 errors.New 回傳錯誤。
指標與值的混淆 &x 取得位址,*p 取得值;寫錯方向會產生編譯錯誤或邏輯錯誤。 明確命名:如 ptrXaddrOfX,讓意圖一目了然。
指標逃逸(escape) 把局部變數的指標回傳,編譯器會將其搬到堆上,可能影響 GC 效能。 若需要長期保存指標,考慮使用 new 或明確的結構體欄位。
不必要的指標傳遞 小型值類型(intfloat64)直接傳值成本低,使用指標會增加複雜度。 只在需要修改原始值或避免大量複製時 使用指標。
指標算術 Go 不支援指標算術(如 p+1),避免 C 語言的低階錯誤。 若需要遍歷陣列,使用切片(slice)或 for range

其他最佳實踐

  1. 使用 make 建立切片、映射與通道,而非手動指標操作。
  2. 盡量讓函式接受值或介面,只有在確實需要改變呼叫端狀態時才使用指標。
  3. 在文件註解中標明指標參數的意圖(例如「*User 為輸入/輸出」),提升可讀性。
  4. 利用 go vetstaticcheck 檢測可能的 nil 指標使用。

實際應用場景

場景 為什麼需要指標? 範例概念
大型結構體傳遞 複製整個結構體會耗費記憶體與 CPU。 func Process(cfg *Config)
資料結構實作(鏈結串列、樹) 每個節點必須保存下一個/子節點的位址。 type Node struct { val int; next *Node }
共享狀態(計數器、緩衝區) 多個 goroutine 必須同時讀寫同一塊記憶體。 var counter int64; atomic.AddInt64(&counter, 1)
函式回傳多值(錯誤處理) 只回傳一個值時,用指標傳回額外資訊。 func Read(p []byte, n *int) error
測試與 Mock 透過指標可以在測試時注入假資料。 func NewDB(conn *sql.DB) *Service

總結

  • 指標是 Go 連結變數與記憶體位址的關鍵& 用於取得位址,* 用於解引用。
  • 正確使用指標可以 減少不必要的資料複製、提升效能,同時讓函式能直接修改呼叫端的資料。
  • 必須留意 nil 指標、指標逃逸與不必要的指標傳遞,遵守最佳實踐可避免常見的 panic 與效能問題。
  • 在實務開發中,指標常見於 大型結構體傳遞、資料結構實作、共享狀態 等情境。

掌握了指標的宣告與操作,你就能在 Go 程式設計的道路上更進一步,寫出既 高效安全 的程式碼。祝你玩得開心,寫程式順利!