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 不變;但直接修改元素會影響原始資料。 |
明確區分 結構改變(append、re‑slice)與 內容改變(索引賦值)。 |
| 忘記傳遞指標導致資料未被更新 | 在需要改變呼叫端變數時,只傳遞值會造成變更失效。 | 使用 & 取得位址,或在方法上使用指標接收者。 |
| 指標為 nil 而未檢查 | 對 nil 指標解引用會 panic。 | 在函式入口處檢查 if p == nil { return },或使用 omitempty 的設計。 |
| 過度使用指標導致資料競爭 | 多個 goroutine 同時寫同一指標會產生 race condition。 | 使用 sync.Mutex、sync.RWMutex 或 channel 進行同步。 |
| 介面值的隱蔽拷貝 | 介面本身是值傳遞,若底層是指標型別,仍會共享;若底層是值型別,則會產生拷貝。 | 依需求選擇介面實作;若需要共享狀態,使用指標型別實作介面。 |
最佳實踐
- 預設使用值傳遞:除非需要修改呼叫端或結構體過大,否則保持簡潔、避免不必要的指標。
- 對大型結構體使用指標:減少拷貝成本,同時注意競爭條件。
- 在方法上根據需求選擇接收者:
- 只讀或小型結構體 → 值接收者。
- 需要修改或結構體較大 → 指標接收者。
- 避免在 slice、map、channel 上做不必要的重新分配:使用
make、copy、append前先檢查容量。 - 使用
go vet、staticcheck、race detector及單元測試 來捕捉指標使用錯誤。
實際應用場景
| 場景 | 為何選擇值傳遞 | 為何選擇引用傳遞 |
|---|---|---|
| 計算函式(純函數) | 輸入不變、無副作用,使用值傳遞可保證安全。 | - |
| 資料庫連線池 | 連線物件本身較大且需共享,使用指標或介面指標。 | |
| 圖形渲染引擎的向量計算 | 向量是小型結構體(float64 x, y, z),值傳遞足夠且可避免指標帶來的 GC 負擔。 | |
| Web 服務的請求上下文(Context) | context.Context 本身是介面,內部實作使用指標,傳遞值即可共享。 |
|
| 大型設定檔(Config) | 設定結構體可能包含多個 slice、map,傳遞指標避免大量拷貝。 | |
| 併發資料結構(如 sync.Map) | 內部已實作同步機制,使用指標或介面指標讓多個 goroutine 共享同一實例。 |
總結
- Go 所有參數預設皆以值傳遞,但 slice、map、channel 內部持有指標,使得表面上看起來像是「引用傳遞」。
- 若希望函式或方法 改變呼叫端的狀態,必須使用 指標(
*T)或 指標接收者。 - 大型結構體、需要共享狀態、或 效能敏感 的情況下,優先考慮指標;而 純計算、不可變資料 則使用值傳遞,以提升程式的可讀性與安全性。
- 掌握這兩種傳遞方式的差異,能幫助開發者在 效能、記憶體使用、併發安全 之間取得最佳平衡,寫出更健全、更易維護的 Go 程式。
關鍵一句話:在 Go 中,「值傳遞」是語言的默認行為,只有在明確需要共享或修改同一塊記憶體時,才使用「指標」。掌握這個原則,你的程式將既快又安全。