防止拷贝问题: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许可证)
 
         
         
         
        
评论