前言
阅读Golang sync包时,总会看到一句话“must not be copied after first use”,对此感到很好奇,查阅过程中发现这篇文章总结得挺到位的,因此转载,记录一下,因为我只是对于原理上面好奇,因此没有全文翻译过来,只挑选了一些自己感兴趣的地方用自己的话总结了一下,感兴趣的可以看看原文章:
What does “nocopy after first use” mean in golang and how
正文
must not be copied after first use
初次使用后不能复制,sync包大多跟并发控制相关,出于安全考虑(避免指针的复制使得指针污染不安全,误操作而使程序崩溃)不能复制可以理解,但Golang是怎么样办到的呢,接下来就从源码层面看看
1. 运行时检测,实例地址值传递
这个是在初次时候后记录变量地址,二次使用时比对变量地址,如果不同的话说明被复制了。
首先,我们先来看一个比较明显的例子strings.Builder
type Builder struct {
addr *Builder // 关键所在,专门用来记录Builder实例的地址
buf []byte
}
func (b *Builder) copyCheck() {
if b.addr == nil {
// 初始化,记录b实例的地址
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
...
}
// test case
var a strings.Builder
a.Write([]byte("testa"))
var b = a
b.Write([]byte("testb")) // 这里是复制后使用,所以会诱发panic
很明显,strings.Builder
通过一个指针来存储实例化后的实例地址,由于这个值是由内部赋值的,所以初次使用时为 nil
,此时会存储地址,下次使用的时候会进行比对,不一致,说明被复制过了
接下来,我们回到sync包,来看看sync.Cond
怎么处理
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
type copyChecker uintptr
func (c *copyChecker) check() {
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}
func (c *Cond) Wait() {
c.checker.check()
...
}
这里跟strings.Builder
有点不一样,因为sync.Cond
通过一个结构体copyChecker
来进行判断处理,咱们来看看关键代码check()
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
我们假设一下创建了一个cond,cond := sync.NewCond(new(sync.Mutex))
,此时假设内存如 "cond内存示例假设图" 第一部分所示。接下来再调用cond.Wait()
后会触发check()
里面的
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c)))
由于此时checker值为0,因此会把checker的地址存储进去,那么此时cond.checker的值为cond.checker的地址(假设是0x04)
接下来当复制该变量condB := cond
时,整块空间会被复制到一个新内存(假设此时checker地址为0x0A),这个时候如果再次调用cond.Wait()
,那么一比对就会发现cond被复制了,于是乎就起到了复制检测的功能
2. 静态代码检测,通过go vet
-copylocks
是go vet的一个flag,用来开启是否有不允许拷贝但被拷贝的代码检测,只需要定义一个结构体noCopy,然后嵌入到你不允许拷贝的结构体。如果你希望自己定义的一个结构体使用者无法拷贝,只能指针传递保证全局唯一的话,也可以使用这个方法处理
// noCopy may be embedded into structs which must not be copied
// after the first use.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) UnLock() {}
实例代码:
// file: test.go
package main
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
// sync.Pool
type Pool struct {
noCopy noCopy
val int
}
func main() {
poolA := Pool{}
poolB := poolA
poolB.val = 1024
}
然后通过命令go vet -copylocks ./test.go
就可以检测到错误
$ go vet -copylocks ./test.go
# command-line-arguments
.\test.go:16:11: assignment copies lock value to poolB: command-line-arguments.Pool contains command-line-arguments.noCopy
回到sync包,我们也能看到一样的身影
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() interface{}
}
...