防止拷贝问题:Go中的不拷贝策略
To change one’s life, start immediately, do it flamboyantly, no exceptions.
— William James
1.1 sync.noCopy类型
在学习 WaitGroup 代码时:WaitGroup You Should Know in Golang | by Wesley Wei | Jul, 2024 | Programmer’s Career,我注意到了 noCopy 类型,而且看到一个很熟悉的注释:”must not be copied after first use”。
1 | // A WaitGroup must not be copied after first use. |
通过搜索,可以发现 “must not be copied after first use” 和 noCopy 类型大都同时出现。
1 | // Note that it must not be embedded, due to the Lock and Unlock methods. |
观察 noCopy Definiton In Go1.23 的定义,可以发现:
- noCopy 类型是空 struct
- noCopy 类型实现了两个方法: Lock 和 Unlock,而且都是空方法(no-op)
- 注释强调了 Lock 和 Unlock 在 go vet 检查时用到
noCopy类型没有任何实质的功能属性,我想只有思考、动手才知道它的具体作用,顺便弄清楚 why “must not be copied after first use”。
1.2 go vet and “locks erroneously passed by value”
当我们输入以下命令:
1 | go tool vet help copylocks |
可以得到相关介绍:
1 | copylocks: check for locks erroneously passed by value |
这告诉我们当使用值传递(copying a value containing a lock)时,可能会出现意想不到的问题,具体会出现什么问题呢?举个例子:
1 | package main |
1 | // output |
这个例子会输出错误,为什么呢?因为func (t T) Lock()
和 func (t T) Unlock()
方法使用值接收者 t
。调用方法时会创建 T
的一个副本,在运行时 Lock()中的 t.lock 和 Unlock() 的t.lock 对应的不是同一个锁实例。
那么如何修复呢?copylocks 的注释里也给出了答案。使用指针接受者,这样就使用了同一个锁实例。
1 | func (t *T) Lock() { |
同理,当我们使用Cond、WaitGroup等时,也需要我们保证对应内容 ”must not be copied after first use“,举一个错误使用WaitGroup的例子:
1 | package main |
如何修复呢?使用同一个wg即可,读到这里不妨动手试试。 更多copylock内容参考:copylock。
1.3 尝试go vet 检测
go vet
的 noCopy
的设计是一种避免结构体被拷贝的机制,尤其适用于那些包含同步原语(如 sync.Mutex
、sync.WaitGroup
)的结构体。意义在于防范不当的 copylocks 发生,但这种防范不是强制的,是否应该拷贝需要开发者自行检测。例如:
1 | package main |
上面这个例子并没用实质性的用处,程序可以正确运行但go vet 会提示“passes lock by value”问题,这里只是动手试用了 go vet 检测的机制。
但如果你需要写一些同步原语相关的代码(类似 sync.Mutex
、sync.WaitGroup
),no Copy的机制可能对你有用。
1.4 其他noCopy策略
由上我们可知 go vet 可以检查非强制的拷贝潜在问题,那有没有强制不允许拷贝的策略呢?有的。首先看一下 strings.Builder
的源代码:
1 |
|
重点是:
1 | b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b))) |
这行代码做了以下几件事:
unsafe.Pointer(b)
:将b
转换为unsafe.Pointer
,以便使用abi.NoEscape
。abi.NoEscape(unsafe.Pointer(b))
:通过abi.NoEscape
函数,告诉编译器b
这个指针不会逃逸,也就是说它可以继续在栈上分配,而不是堆上。(*Builder)(…)
:将abi.NoEscape
返回的unsafe.Pointer
再转换回*Builder
类型,以便能够正常使用。- 最后,
b.addr
被设置为b
自身的地址,这样可以防止Builder
被拷贝(在后面有检查b.addr != b
的逻辑)。
go1.23.0 builder.go
abi.NoEscape
针对 strings.Builder 的拷贝行为会发生panic:
1 | func main() { |
Another example is sync.Cond
,具体可以参考:What does “nocopy after first use” mean in golang and how | by Jing | Medium
1.5 总结
- 同步原语(如
sync.Mutex
、sync.WaitGroup
)不应被拷贝,因为一旦被拷贝,它们的内部状态会被复制,从而引发并发问题。 - 虽然 Go 语言本身没有提供一种强制防止拷贝的机制,但
noCopy
结构体提供了一种非强制的防拷贝机制,主要用于标识和go vet
工具静态检测。 - Go语言的某些源代码在runtime时会进行 noCopy的检查并返回panic,像
strings.Builder
和sync.Cond
。
参考
Detect locks passed by value in Go | by Michał Łowicki | golangspec | Medium
What does “nocopy after first use” mean in golang and how | by Jing | Medium
更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
English post: https://programmerscareer.com/golang-nocopy/
作者:Wesley Wei – Twitter Wesley Wei – Medium
发表日期:原文在 2024-07-13 21:26 时创作于 https://programmerscareer.com/golang-nocopy/.
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
评论