本文 AI 產出,尚未審核

Golang

單元:指標與記憶體管理

主題:指標(pointers)的基本概念


簡介

在 Go 語言中,指標是連結變數與實際記憶體位置的橋樑。雖然 Go 內建了自動垃圾回收(GC),但了解指標的運作方式仍是寫出高效、可讀程式碼的關鍵。指標讓我們能:

  1. 避免不必要的資料拷貝,提升效能。
  2. 共享同一塊記憶體,讓多個函式或 goroutine 能同時觀察或修改資料。
  3. 實作資料結構(如鏈結串列、樹)時,必須依賴指標才能正確連接節點。

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整呈現指標在 Go 中的使用方式,適合 初學者 了解基礎,也能為 中級開發者 打下更穩固的記憶體管理觀念。


核心概念

1. 什麼是指標

指標是一個變數,裡面存放的是另一個變數的記憶體位址(address)。在 Go 中,指標型別以 *T 表示,代表「指向 T 型別的指標」。

var a int = 10        // a 是一個 int 變數,值為 10
var p *int = &a       // p 是 *int,指向 a 的位址
  • & 取得變數的位址(取址運算子)。
  • * 用於宣告指標型別或解引用(取得指標指向的值)。

注意:Go 不允許指標算術(pointer arithmetic),這是為了避免記憶體安全問題。


2. 取得指標與解引用

以下範例展示如何取得指標、修改指標指向的值,以及解引用的差別。

package main

import "fmt"

func main() {
    x := 5               // 普通變數
    px := &x             // 取得 x 的位址,px 的型別是 *int

    fmt.Println("x 的值 :", x)          // 5
    fmt.Println("px 的位址 :", px)       // 0x...(實際位址)
    fmt.Println("*px 的值 :", *px)       // 5,解引用取得 x 的值

    *px = 20            // 直接修改指標指向的記憶體內容
    fmt.Println("修改後的 x :", x)      // 20
}

重點:透過 *px = 20,我們改變了 x 本身的值,而不需要把 x 作為參數傳回函式。


3. nil 指標

未被初始化的指標預設為 nil,表示它不指向任何有效的記憶體位置。使用 nil 指標前必須先檢查,否則會產生 runtime panic

var p *int          // p 為 nil
if p == nil {
    fmt.Println("p 為 nil,不能直接解引用")
}

// 正確的做法:先分配記憶體
p = new(int)        // 等同於 p = &int{}
*p = 7
fmt.Println(*p)    // 7

new(T) 會在堆上分配一塊零值(zero value)的記憶體,並回傳指向它的 *T


4. 指標與結構體

結構體常與指標一起使用,因為傳值會把整個結構體複製一遍,對於大型結構體會浪費記憶體與 CPU 時間。

type Person struct {
    Name string
    Age  int
}

// 傳值(會複製整個 Person)
func UpdateByValue(p Person) {
    p.Age = 30
}

// 傳指標(只改變原始資料)
func UpdateByPointer(p *Person) {
    p.Age = 30
}

func main() {
    alice := Person{Name: "Alice", Age: 25}
    UpdateByValue(alice)
    fmt.Println(alice.Age) // 仍然是 25

    UpdateByPointer(&alice)
    fmt.Println(alice.Age) // 變成 30
}

建議:除非結構體非常小(如只包含幾個基本型別),否則一律使用指標傳遞。


5. 指標與陣列 / slice

  • 陣列:陣列本身是值類型,傳遞時會複製整個陣列。若想避免複製,可傳遞陣列的指標 *[N]T
  • slice:slice 本身已經是一個 描述子(指向底層陣列的指標、長度、容量),傳遞 slice 時不會複製底層資料,只會複製描述子。因此大多數情況下不需要額外的指標。
func modifyArray(a *[3]int) {
    a[0] = 100
}

func modifySlice(s []int) {
    s[0] = 100
}

func main() {
    arr := [3]int{1, 2, 3}
    modifyArray(&arr)
    fmt.Println(arr) // [100 2 3]

    slc := []int{1, 2, 3}
    modifySlice(slc)
    fmt.Println(slc) // [100 2 3]
}

常見陷阱與最佳實踐

陷阱 說明 改善方式
指標指向已釋放的物件 雖然 Go 有 GC,但若持有指向 局部變數 的指標,可能導致意外的記憶體保留(memory leak) 使用 newmake 或返回指標時確保變數的生命週期足夠長
nil 指標解引用 直接 *p 會觸發 panic 在使用前 if p == nil { … }
不必要的指標 小型結構體或基本型別直接傳值更簡潔 只在需要共享或避免大量拷貝時才使用指標
指標與介面混用 介面值本身是兩個指標(type、data),再包一層指標會增加複雜度 盡量使用介面本身,不必額外加 *interface{}
指標算術 Go 不支援,嘗試會編譯錯誤 改用 sliceappendcopy 等安全操作

最佳實踐

  1. 盡量使用 make 為 slice、map、channel 分配記憶體,避免手動 new 後再轉型。
  2. 函式接受指標時,文件要說明它會修改呼叫者的資料,提升可讀性。
  3. 使用 defer 搭配 recover 處理可能的 nil 解引用 panic,避免程式崩潰。
  4. 在結構體內部使用指標時,考慮零值(zero value)是否足以代表「未設定」,若是,直接使用值類型即可。

實際應用場景

  1. 資料庫模型的 CRUD

    func UpdateUser(u *User) error {
        // 直接修改傳入的 User 結構體
        u.UpdatedAt = time.Now()
        return db.Save(u).Error
    }
    

    透過指標,資料庫層可以直接寫回修改後的欄位,而不必再回傳整個結構體。

  2. 共享配置(Config)
    應用程式啟動時載入一次配置,之後各模組都持有同一個指標,變更時即時生效。

  3. 實作鏈結串列、樹等資料結構
    每個節點都需要指向下一個節點的指標,若使用值類型會導致遞迴拷貝,效能極差。

  4. 高頻率的函式呼叫
    在大量計算的迴圈裡傳遞大型結構體的指標,可減少 GC 壓力與記憶體搬移。


總結

  • 指標是 Go 中連結變數與記憶體位址的關鍵,能夠避免不必要的拷貝、共享資料、實作複雜結構。
  • 取得指標使用 &,解引用使用 *;未初始化的指標為 nil,使用前必須檢查。
  • 結構體、陣列、slice 等情境下,依需求選擇是否使用指標:結構體多使用指標、slice 已內建指向底層陣列的描述子。
  • 常見陷阱包括 nil 解引用、過度使用指標、指標指向已失效的物件,遵守最佳實踐能讓程式更安全、可維護。
  • 資料庫操作、全域設定、資料結構實作 等實務場景中,指標是不可或缺的工具。

掌握了指標的基本概念與正確使用方式,你就能在 Go 專案中寫出更高效、更可靠的程式碼。祝你在 Golang 的指標與記憶體管理之路上,越走越順!