
为什么 Go 里不用手动写引用计数
Go 的运行时自带垃圾回收(GC),对象生命周期由 GC 自动管理,new、make 出来的堆对象不需要你维护引用数。强行用指针模拟引用计数,不仅多余,还容易破坏 GC 的追踪逻辑——比如通过 unsafe.Pointer 绕过 GC,可能导致对象被提前回收或内存泄漏。
真正需要“引用感知”的场景,通常是:封装 C 资源(如文件描述符、OpenGL 纹理)、复用大内存块、或对接没有 GC 的外部系统。这时不是“用 Go 指针实现引用计数”,而是用 Go 类型封装 + 显式生命周期控制。
用 sync.WaitGroup 或原子计数器替代裸指针计数
如果你确实要跟踪某个资源被多少 goroutine 共享,别用 *int 手动 ++/--——竞态风险高,且无法和资源释放联动。
- 对轻量共享状态(如连接是否关闭),用
sync/atomic.Int64安全增减,配合CompareAndSwap做释放判断 - 对需等待所有使用者退出的场景(如共享 buffer),用
sync.WaitGroup更直观:每次获取资源调wg.Add(1),释放时wg.Done(),最后wg.Wait()再回收 - 避免把计数器字段暴露给调用方;应封装在结构体里,只提供
Acquire()/Release()方法
示例:一个带引用计数的字节池
立即学习“go语言免费学习笔记(深入)”;
type RefBuf struct {
data []byte
refs sync.WaitGroup
}
func (b *RefBuf) Acquire() {
b.refs.Add(1)
}
func (b *RefBuf) Release() {
b.refs.Done()
}
func (b *RefBuf) Close() {
b.refs.Wait()
// 此时可安全归还 data 到 sync.Pool 或 free
}
对接 C 资源时,用 runtime.SetFinalizer 补充清理
当 Go 代码持有 C 分配的内存(如 C.malloc)或句柄(如 open() 返回的 fd),必须确保它被释放。仅靠引用计数不够,因为 Go GC 不知道这些 C 资源的存在。
- 引用计数决定“谁还能用”,但最终释放时机得靠
runtime.SetFinalizer保底:对象被 GC 时触发清理,防止泄漏 - Finalizer 不能假设执行顺序或时间,所以它只是兜底,核心逻辑仍应靠显式
Close()或Release() - 不要在 Finalizer 里做阻塞操作(如网络请求、锁等待),否则会拖慢 GC
常见错误:只实现引用计数,忘了设 Finalizer → 程序长时间运行后 C 内存爆满。
别把 *T 当引用计数工具,结构体字段才是关键
有人试图这样写:
type Counter struct {
count *int
}
func NewCounter() *Counter {
c := new(Counter)
c.count = new(int)
return c
}
这毫无意义:*int 本身不绑定生命周期,多个 Counter 可能共用同一 *int,也可能各自 new 出来却从不释放。真正可控的是结构体字段的可见性和访问路径。
- 把计数逻辑藏在结构体方法里,字段类型用
int64或sync.WaitGroup,而不是裸指针 - 如果资源需跨 goroutine 共享,结构体本身应是线程安全的,或明确要求调用方加锁
- 导出的 API 避免返回内部指针(如
func (*Counter) CountPtr() *int),否则使用者可能绕过你的控制逻辑
复杂点在于:引用计数只是资源管理的一环,它解决不了循环引用、异步释放时机、或 C 资源与 Go 对象生命周期错位的问题。这些地方一松懈,就不是少写个 ++ 的事了。










