Go 1.24 中的弱指針包 weak 使用
在 Go 語言中,“弱指針”指的是不會阻止垃圾回收器(GC)回收目標對象的引用。
當一個對象只剩弱指針指向它,而沒有任何強引用時,GC 仍會把該對象當作不可達對象并回收;隨后,所有指向它的弱指針會自動變為 nil。
簡而言之,弱指針不會增加對象的引用計數。當一個對象只被弱指針引用時,垃圾回收器就可以釋放它。因此,在嘗試使用弱指針的值之前,應檢查它是否為 nil。
Go 1.24 中的 weak 包
Go 1.24 新增 weak 包,提供了創建和使用弱指針的簡潔 API。
import "weak"
type MyStruct struct {
Data string
}
func main() {
obj := &MyStruct{Data: "example"}
wp := weak.Make(obj) // 創建弱指針
val := wp.Value() // 獲取強引用或 nil
if val != nil {
fmt.Println(val.Data)
} else {
fmt.Println("對象已被垃圾回收")
}
}
在以上示例中,weak.Make(obj) 創建了指向 obj 的弱指針。調用 wp.Value() 時,如果對象仍存活則返回強引用,否則返回 nil。
測試弱指針:
import (
"fmt"
"runtime"
"weak"
)
type MyStruct struct {
Data string
}
func main() {
obj := &MyStruct{Data: "test"}
wp := weak.Make(obj)
obj = nil // 移除強引用
runtime.GC()
if wp.Value() == nil {
fmt.Println("對象已被垃圾回收")
} else {
fmt.Println("對象仍然存活")
}
}
通過將強引用 obj 置為 nil 并主動觸發 GC,可觀察到弱指針在對象被回收后返回 nil 的行為。
弱指針”與“強引用”的區別
特性 | 強引用 (*T) | 弱引用 (weak.Pointer[T]) |
影響 GC | 會保持對象存活 | 不會保持對象存活 |
空值 | nil | nil (目標被回收或從未賦值) |
訪問方式 | 直接解引用 | 先調用 Value() |
示例 1: 弱指針做臨時緩存
使用弱指針的一個典型場景是在緩存中存儲條目,同時不阻止它們被 GC 回收。
package main
import (
"fmt"
"runtime"
"sync"
"weak"
)
type User struct {
Name string
}
var cache sync.Map // map[int]weak.Pointer[*User]
func GetUser(id int) *User {
// ① 先從緩存里取
if wp, ok := cache.Load(id); ok {
if u := wp.(weak.Pointer[User]).Value(); u != nil {
fmt.Println("cache hit")
return u
}
}
// ② 真正加載(這里直接構造)
u := &User{Name: fmt.Sprintf("user-%d", id)}
cache.Store(id, weak.Make(u))
fmt.Println("load from DB")
return u
}
func main() {
u := GetUser(1) // load from DB
fmt.Println(u.Name)
runtime.GC() // 即使立刻 GC,因 main 持有強引用,User 仍在
u = nil // 釋放最后一個強引用
runtime.GC() // 觸發 GC,User 可能被回收
_ = GetUser(1) // 如被回收,會再次 load from DB
}
在該緩存實現中,條目以弱指針形式存儲。如果對象沒有其他強引用,GC 可以將其回收;下次調用 GetUser 時,數據會被重新加載。
運行上述代碼,輸出如下:
$ go run cache.go
load from DB
user-1
load from DB
為什么要使用弱指針?
常見場景包括:
- 緩存:在不強制對象常駐內存的前提下存儲它們,如果其他地方不再使用,對象就能被回收;
- 觀察者模式:保存對觀察者的引用,同時不阻止它們被 GC 回收;
- 規范化(Canonicalization):確保同一對象只有一個實例,并且在不再使用時可被回收;
- 依賴關系圖:在樹或圖等結構中避免形成引用環。
弱指針使用注意事項
- 隨時檢查 nil:對象可能在任意 GC 周期后被回收,Value() 結果不可緩存。
- 避免循環依賴:不要讓弱指針中的對象重新持有創建它的容器,否則仍會形成強引用鏈。
- 性能權衡:訪問弱指針需要額外調用,且頻繁從 nil 狀態恢復對象會導致抖動。
示例 2:強指針的普通使用
package main
import (
"fmt"
"runtime"
)
type Session struct {
ID string
}
func main() {
s := new(Session) // 與 &Session{} 等價
s.ID = "abc123"
fmt.Println("strong ref alive:", s.ID)
s = nil // 取消最后一個強引用
runtime.GC() // 嘗試觸發 GC(僅演示,實際時機由運行時決定)
fmt.Println("done")
}
這里的 s 就是強指針,只要它仍然可達,Session 對象就絕不會被 GC 回收。
強指針指向的對象何時被 GC?
(1) 可達性判定:Go 使用標記-清除式 GC。一次 GC 周期開始時,運行時會從根對象(棧、全局變量、當前寄存器等)向外遍歷所有強引用。
- 能通過強引用鏈到達的對象稱為可達 (reachable),一定存活;
- 其余對象被標記為不可達 (unreachable),在清掃階段釋放。
(2) 不存在“引用計數”:只有“是否從根可達”這一條件;變量數目多少、值是否相等都不影響回收;
(3) 時間點不確定:GC 周期由調度器自動觸發,開發者只能調用 runtime.GC() 進行“建議”式觸發,不能保證立即回收;
(4) 變量本身也會被 GC:若強指針變量 s 位于堆上且其所在結構不再可達,那么 s 本身也會被 GC;棧變量則隨函數返回被回收。
總結一句:強指針保證可達對象在任意 GC 周期都處于“存活集合”中;一旦最后的強引用鏈斷開,對象就會在下一個 GC 周期被自動釋放。