本文 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 取得值;寫錯方向會產生編譯錯誤或邏輯錯誤。 |
明確命名:如 ptrX、addrOfX,讓意圖一目了然。 |
| 指標逃逸(escape) | 把局部變數的指標回傳,編譯器會將其搬到堆上,可能影響 GC 效能。 | 若需要長期保存指標,考慮使用 new 或明確的結構體欄位。 |
| 不必要的指標傳遞 | 小型值類型(int、float64)直接傳值成本低,使用指標會增加複雜度。 |
只在需要修改原始值或避免大量複製時 使用指標。 |
| 指標算術 | Go 不支援指標算術(如 p+1),避免 C 語言的低階錯誤。 |
若需要遍歷陣列,使用切片(slice)或 for range。 |
其他最佳實踐
- 使用
make建立切片、映射與通道,而非手動指標操作。 - 盡量讓函式接受值或介面,只有在確實需要改變呼叫端狀態時才使用指標。
- 在文件註解中標明指標參數的意圖(例如「
*User為輸入/輸出」),提升可讀性。 - 利用
go vet與staticcheck檢測可能的 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 程式設計的道路上更進一步,寫出既 高效 又 安全 的程式碼。祝你玩得開心,寫程式順利!