本文 AI 產出,尚未審核

Golang – 函數與方法

主題:參數傳遞(值傳遞 vs. 引用傳遞)


簡介

在 Go 語言中,函式(function)方法(method) 是程式設計的核心抽象。它們負責把程式切割成可重複使用、易於測試的單位,而參數傳遞方式則直接影響程式的效能、記憶體使用與行為正確性。

  • 值傳遞(pass‑by‑value):呼叫端會將參數的 副本 傳給函式,函式內的修改不會影響原始變數。
  • 引用傳遞(pass‑by‑reference):透過指標(pointer)或 slice、map、channel 等內建參考型別,把 同一塊記憶體 傳入,函式內的變更會直接反映到呼叫端。

了解這兩種傳遞機制的差異,能幫助我們在 效能安全性可讀性 之間取得平衡,寫出既快又不易出錯的 Go 程式。


核心概念

1. Go 的預設傳遞方式:值傳遞

Go 語言的所有參數在語法層面上都是 值傳遞。即使是 slice、map、channel 這類「看起來像是引用」的型別,實際上仍然是把 描述資料結構的值(包括指向底層陣列的指標、長度、容量)複製一份傳入函式。

func increment(v int) {
    v++               // 只改變副本
}

func main() {
    x := 10
    increment(x)
    fmt.Println(x)    // 輸出 10,x 本身未被改變
}

重點:對於基本型別(int、float、bool、string)以及自訂的 struct,傳遞的永遠是 副本


2. 使用指標實現引用傳遞

若希望函式內部能改變呼叫端的變數,就需要傳遞 指標*T)。指標本身也是一個值(記憶體位址),但它指向的是真正的資料。

func incrementPtr(p *int) {
    *p++               // 直接操作原始記憶體
}

func main() {
    x := 10
    incrementPtr(&x)   // 傳入 x 的位址
    fmt.Println(x)    // 輸出 11
}
  • &x 取得變數 x 的位址(指標)。
  • *p 取得指標指向的值,進行讀寫。

注意:指標傳遞不等同於「傳遞整個物件的指標」的概念,仍然是把指標值(位址) 複製 給函式,只是兩個副本指向同一塊記憶體。


3. Slice、Map、Channel:內建的引用型別

這三種型別在底層都有指向資料的指標,因此即使是「值傳遞」,實際上也會產生 共享的底層結構

func appendSlice(s []int) []int {
    s = append(s, 100) // 只改變副本的 slice 結構
    return s
}

func modifySlice(s []int) {
    s[0] = 999         // 直接改變底層陣列
}

func main() {
    a := []int{1, 2, 3}
    b := appendSlice(a) // a 本身不變,b 為新 slice
    fmt.Println(a)      // [1 2 3]
    fmt.Println(b)      // [1 2 3 100]

    modifySlice(a)      // a 的第一個元素被改變
    fmt.Println(a)      // [999 2 3]
}
  • appendSlice 中的 s = append(s, 100) 會產生 新 slice(若容量不足),原始 a 不受影響。
  • modifySlice 直接改變底層陣列,所有指向同一陣列的 slice 都會看到變化。

4. 方法接收者:值接收者 vs. 指標接收者

在定義 方法 時,我們可以選擇 值接收者func (t T) Method()) 或 指標接收者func (t *T) Method())。兩者的行為與上面的函式傳遞相同。

type Counter struct {
    value int
}

// 值接收者:不會改變原始物件
func (c Counter) Add(v int) {
    c.value += v
}

// 指標接收者:會改變原始物件
func (c *Counter) AddPtr(v int) {
    c.value += v
}

func main() {
    c1 := Counter{value: 5}
    c1.Add(3)
    fmt.Println(c1.value) // 仍是 5

    c1.AddPtr(3)
    fmt.Println(c1.value) // 變成 8
}

建議:若方法需要修改接收者的狀態,或接收者本身較大(避免每次呼叫都拷貝),應使用 指標接收者


5. 常見的「看起來是值傳遞」陷阱

5.1. 結構體內嵌指標

type Node struct {
    Data int
    Next *Node
}

func modifyNode(n Node) {
    n.Data = 999          // 只改變副本的 Data
    n.Next = nil          // 只改變副本的 Next 指標
}

即使 Node 包含指標欄位,傳遞 Node 本身仍是 值傳遞,所以對 n.Data 的改變不會影響原始結構。但若直接改變 n.Next.Data,則會透過指標改變底層結構。

5.2. Interface 的隱蔽拷貝

type Writer interface {
    Write(p []byte) (n int, err error)
}

func replaceWriter(w Writer) {
    // 這裡的 w 仍是值傳遞,但底層可能是指標實作
}

介面本身是 兩個字(type、value)的組合,傳遞時會拷貝這兩個字。若底層實作是指標型別,仍會共享同一個實例。


程式碼範例(實用示例)

範例 1:交換兩個整數(指標版)

func swap(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 1, 2
    swap(&x, &y)
    fmt.Printf("x=%d, y=%d\n", x, y) // x=2, y=1
}

說明:使用指標可以直接改變呼叫端的變數,避免回傳多個值。


範例 2:累加 slice 中的所有元素(值傳遞版)

func sumSlice(s []int) int {
    total := 0
    for _, v := range s {
        total += v
    }
    return total
}

func main() {
    data := []int{1, 2, 3, 4}
    fmt.Println(sumSlice(data)) // 10
    fmt.Println(data)           // 原 slice 不變
}

說明:即使 s 是 slice,傳遞的是 slice 的描述值(指標+長度+容量),不會改變底層陣列。


範例 3:在 slice 中原地修改(引用行為)

func doubleInPlace(s []int) {
    for i := range s {
        s[i] *= 2
    }
}

func main() {
    nums := []int{1, 2, 3}
    doubleInPlace(nums)
    fmt.Println(nums) // [2 4 6]
}

說明:雖然 s 是值傳遞,但它指向同一個底層陣列,修改會直接影響 nums


範例 4:使用指標接收者實作計數器

type Counter struct {
    total int
}

// 只讀方法(值接收者)
func (c Counter) Value() int {
    return c.total
}

// 變更方法(指標接收者)
func (c *Counter) Add(delta int) {
    c.total += delta
}

func main() {
    var c Counter
    c.Add(5)          // 編譯錯誤!必須使用指標
    (&c).Add(5)       // 正確
    fmt.Println(c.Value()) // 5
}

說明:指標接收者允許方法修改結構體內部狀態;若使用值接收者,編譯器會提示「cannot assign to c.total」的錯誤。


範例 5:大型結構體的效能考量(指標 vs. 值)

type Big struct {
    data [1024]byte
}

// 以值傳遞:每次呼叫都會拷貝 1KB
func processValue(b Big) {
    // 只讀操作
}

// 以指標傳遞:只拷貝指標(8 bytes)
func processPtr(b *Big) {
    // 讀寫皆可
}

func main() {
    var big Big
    processValue(big) // 複製成本較高
    processPtr(&big)  // 輕量
}

說明:當結構體較大或頻繁呼叫時,使用指標 可以顯著降低記憶體拷貝成本。


常見陷阱與最佳實踐

陷阱 說明 解決方案
誤以為 slice 會「值傳遞」而不會改變原始資料 append 產生新 slice 時,原始 slice 不變;但直接修改元素會影響原始資料。 明確區分 結構改變appendre‑slice)與 內容改變(索引賦值)。
忘記傳遞指標導致資料未被更新 在需要改變呼叫端變數時,只傳遞值會造成變更失效。 使用 & 取得位址,或在方法上使用指標接收者。
指標為 nil 而未檢查 對 nil 指標解引用會 panic。 在函式入口處檢查 if p == nil { return },或使用 omitempty 的設計。
過度使用指標導致資料競爭 多個 goroutine 同時寫同一指標會產生 race condition。 使用 sync.Mutexsync.RWMutex 或 channel 進行同步。
介面值的隱蔽拷貝 介面本身是值傳遞,若底層是指標型別,仍會共享;若底層是值型別,則會產生拷貝。 依需求選擇介面實作;若需要共享狀態,使用指標型別實作介面。

最佳實踐

  1. 預設使用值傳遞:除非需要修改呼叫端或結構體過大,否則保持簡潔、避免不必要的指標。
  2. 對大型結構體使用指標:減少拷貝成本,同時注意競爭條件。
  3. 在方法上根據需求選擇接收者
    • 只讀或小型結構體 → 值接收者
    • 需要修改或結構體較大 → 指標接收者
  4. 避免在 slice、map、channel 上做不必要的重新分配:使用 makecopyappend 前先檢查容量。
  5. 使用 go vetstaticcheckrace detector 及單元測試 來捕捉指標使用錯誤。

實際應用場景

場景 為何選擇值傳遞 為何選擇引用傳遞
計算函式(純函數) 輸入不變、無副作用,使用值傳遞可保證安全。 -
資料庫連線池 連線物件本身較大且需共享,使用指標或介面指標。
圖形渲染引擎的向量計算 向量是小型結構體(float64 x, y, z),值傳遞足夠且可避免指標帶來的 GC 負擔。
Web 服務的請求上下文(Context) context.Context 本身是介面,內部實作使用指標,傳遞值即可共享。
大型設定檔(Config) 設定結構體可能包含多個 slice、map,傳遞指標避免大量拷貝。
併發資料結構(如 sync.Map) 內部已實作同步機制,使用指標或介面指標讓多個 goroutine 共享同一實例。

總結

  • Go 所有參數預設皆以值傳遞,但 slice、map、channel 內部持有指標,使得表面上看起來像是「引用傳遞」。
  • 若希望函式或方法 改變呼叫端的狀態,必須使用 指標*T)或 指標接收者
  • 大型結構體需要共享狀態、或 效能敏感 的情況下,優先考慮指標;而 純計算、不可變資料 則使用值傳遞,以提升程式的可讀性與安全性。
  • 掌握這兩種傳遞方式的差異,能幫助開發者在 效能記憶體使用併發安全 之間取得最佳平衡,寫出更健全、更易維護的 Go 程式。

關鍵一句話:在 Go 中,「值傳遞」是語言的默認行為,只有在明確需要共享或修改同一塊記憶體時,才使用「指標」。掌握這個原則,你的程式將既快又安全。