在 Go 语言中,"弱指针"指的是不会阻止垃圾回收器(GC)回收目标对象的引用。
当一个对象只剩弱指针指向它,而没有任何强引用时,GC 仍会把该对象当作不可达对象并回收;随后,所有指向它的弱指针会自动变为 nil
。
简而言之,弱指针不会增加对象的引用计数。当一个对象只被弱指针引用时,垃圾回收器就可以释放它。因此,在尝试使用弱指针的值之前,应检查它是否为 nil
。
Go 1.24 中的 weak 包
Go 1.24 新增 weak 包,提供了创建和使用弱指针的简洁 API。
go
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
。
测试弱指针
go
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 回收。
go
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
时,数据会被重新加载。
运行上述代码,输出如下:
bash
$ go run cache.go
load from DB
user-1
load from DB
为什么要使用弱指针?
常见场景包括:
- 缓存:在不强制对象常驻内存的前提下存储它们,如果其他地方不再使用,对象就能被回收;
- 观察者模式:保存对观察者的引用,同时不阻止它们被 GC 回收;
- 规范化(Canonicalization):确保同一对象只有一个实例,并且在不再使用时可被回收;
- 依赖关系图:在树或图等结构中避免形成引用环。
弱指针使用注意事项
- 随时检查
nil
:对象可能在任意 GC 周期后被回收,Value()
结果不可缓存。 - 避免循环依赖:不要让弱指针中的对象重新持有创建它的容器,否则仍会形成强引用链。
- 性能权衡:访问弱指针需要额外调用,且频繁从
nil
状态恢复对象会导致抖动。
示例 2:强指针的普通使用
go
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?
- 可达性判定:Go 使用标记-清除式 GC。一次 GC 周期开始时,运行时会从根对象(栈、全局变量、当前寄存器等)向外遍历所有强引用 。
- 能通过强引用链到达的对象称为可达 (reachable),一定存活;
- 其余对象被标记为不可达 (unreachable),在清扫阶段释放。
- 不存在"引用计数":只有"是否从根可达"这一条件;变量数目多少、值是否相等都不影响回收;
- 时间点不确定:GC 周期由调度器自动触发,开发者只能调用
runtime.GC()
进行"建议"式触发,不能保证立即回收; - 变量本身也会被 GC:若强指针变量
s
位于堆上且其所在结构不再可达,那么s
本身也会被 GC;栈变量则随函数返回被回收。
总结一句:强指针保证可达对象在任意 GC 周期都处于"存活集合"中;一旦最后的强引用链断开,对象就会在下一个 GC 周期被自动释放。