Golang 中你应该知道的 noCopy 策略
1.1 sync.noCopy 类型
在学习 WaitGroup
代码时,我注意到了 noCopy
类型,并且看到一个很熟悉的注释:"must not be copied after first use"。
// A WaitGroup must not be copied after first use.
//
// In the terminology of the Go memory model, a call to Done
// “synchronizes before” the return of any Wait call that it unblocks.
type WaitGroup struct {
noCopy noCopy
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
sema uint32
}
通过搜索,可以发现“must not be copied after first use”和 noCopy
类型经常同时出现。
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
观察 noCopy
的定义,我们可以得出以下几点:
noCopy
类型是空的结构体。noCopy
类型实现了两个方法:Lock
和Unlock
,而且都是空方法(即 no-op)。- 注释中强调了
Lock
和Unlock
在go vet
检查时起作用。
noCopy
类型没有任何实际功能属性,要理解它的作用,必须从防止结构体被拷贝的需求入手,并探究为什么“must not be copied after first use”。
1.2 go vet 和 "locks erroneously passed by value"
我们可以通过以下命令查看 go vet
中的 copylocks
检查:
go tool vet help copylocks
go vet
告诉我们,包含锁的值在传递时可能会导致问题。举例:
package main
import (
"fmt"
"sync"
)
type T struct {
lock sync.Mutex
}
func (t T) Lock() {
t.lock.Lock()
}
func (t T) Unlock() {
t.lock.Unlock()
}
func main() {
var t T
t.Lock()
fmt.Println("test")
t.Unlock()
fmt.Println("finished")
}
此代码会输出错误,因为 Lock
和 Unlock
方法使用了值接收者,导致 T
被复制,每次调用方法时实际上是对副本加锁解锁。
修复方法: 使用指针接收者,确保操作的是同一个实例:
func (t *T) Lock() {
t.lock.Lock()
}
func (t *T) Unlock() {
t.lock.Unlock()
}
同样,在使用 WaitGroup
时,我们也必须确保不拷贝它的值。如下是一个错误的使用例子:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, wg) // passes lock by value
}
wg.Wait()
fmt.Println("All workers done!")
}
修复方法: 传递指针以使用同一个 WaitGroup
实例:
go worker(i, &wg)
1.3 尝试 go vet 检测
go vet
中的 noCopy
设计是一种防止结构体被拷贝的机制,特别适用于包含同步原语的结构体(如 sync.Mutex
、sync.WaitGroup
)。以下是一个简单的 go vet
检测示例:
package main
import "fmt"
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type noCopyData struct {
Val int32
noCopy
}
func main() {
c1 := noCopyData{Val: 10}
c2 := c1
c2.Val = 20
fmt.Println(c1, c2)
}
尽管代码运行正常,但 go vet
会提示 "passes lock by value"。这是因为 noCopy
的存在,确保开发者意识到潜在的拷贝问题。
1.4 其他 noCopy 策略
除了 go vet
的静态检测,Go 语言中也有一些更强制的防拷贝机制。以 strings.Builder
为例,它在运行时检查是否发生了非法拷贝:
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
func (b *Builder) copyCheck() {
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()
b.buf = append(b.buf, p...)
return len(p), nil
}
这段代码通过 b.addr
的自引用来防止 Builder
被拷贝。
1.5 总结
- 同步原语(如
sync.Mutex
、sync.WaitGroup
)不应被拷贝,否则会引发并发问题。 noCopy
提供了一种非强制的防拷贝机制,通过go vet
工具进行静态检测。- 某些 Go 源代码会在运行时进行更严格的检查,例如
strings.Builder
和sync.Cond
,一旦检测到拷贝行为,将触发panic
。