1. Sync.noCopy
在学习 Go 的 WaitGroup
代码时,我注意到了 noCopy
,并看到一个熟悉的注释:"首次使用后不得复制"。
golang
代码解读
复制代码
// 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 }
搜索后发现,"首次使用后不得复制" 经常和 noCopy
一起出现。
golang
代码解读
复制代码
// 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() {}
通过查看 Go 1.23 中 noCopy 的定义发现:
noCopy
类型是一个空结构体。noCopy
类型实现了两种方法:Lock
和Unlock
,这两种方法都是非操作方法。- 注释强调,
Lock
和Unlock
由go vet
检查器使用。
noCopy
类型没有实际的功能特性,只有通过思索和实验才能理解其具体用途,以及为什么 "首次使用后不得复制"?
2. Go Vet 和 "锁值错误传递"
当我们输入以下命令:
bash
代码解读
复制代码
go tool vet help copylocks
输出:
vbnet
代码解读
复制代码
copylocks: check for locks erroneously passed by value Inadvertently copying a value containing a lock, such as sync.Mutex or sync.WaitGroup, may cause both copies to malfunction. Generally such values should be referred to through a pointer.
Go Vet 告诉我们在使用包含锁(如 sync.Mutex
或 sync.WaitGroup
)的值并通过值传递时,可能会导致意想不到的问题。例如:
golang
代码解读
复制代码
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") }
运行这段代码,将输出错误信息:
go
代码解读
复制代码
// output test fatal error: sync: unlock of unlocked mutex goroutine 1 [running]: sync.fatal({0x4b2c9b?, 0x4a14a0?}) /usr/local/go-faketime/src/runtime/panic.go:1031 +0x18 // ❯ go vet . # noCopy ./main.go:12:9: Lock passes lock by value: noCopy.T contains sync.Mutex ./main.go:15:9: Unlock passes lock by value: noCopy.T contains sync.Mutex Copy
错误原因是 Lock
和 Unlock
方法使用了值接收器 t
,在调用方法时会创建 T
的副本,这意味着 Unlock
中的锁实例与 Lock
中的锁实例不匹配。
为了解决这个问题,可以将接收器改为指针类型:
golang
代码解读
复制代码
func (t *T) Lock() { t.lock.Lock() } func (t *T) Unlock() { t.lock.Unlock() }
同样,在使用 Cond
、WaitGroup
和其他包含锁的类型时,需要确保它们在首次使用后不会被复制。例如:
golang
代码解读
复制代码
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) } wg.Wait() fmt.Println("All workers done!") }
运行这段代码,也会输出错误信息:
dart
代码解读
复制代码
/ Worker 3 starting Worker 1 starting Worker 2 starting Worker 1 done Worker 3 done Worker 2 done fatal error: all goroutines are asleep - deadlock! goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000108040?) // ❯ go vet . # noCopy ./main.go:9:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy ./main.go:21:16: call of worker copies lock value: sync.WaitGroup contains sync.noCopy
要解决这个问题,可以使用相同的 wg
实例,大家可以自己试一下。有关 copylocks 的更多信息可以查看 golang 官网。
3. 尝试 go vet 检测
go vet
的 noCopy
机制是一种防止结构体被拷贝的方法,尤其是那些包含同步原语(如 sync.Mutex
和 sync.WaitGroup
)的结构,目的是防止意外的锁拷贝,但这种防止并不是强制性的,是否拷贝需要由开发者检测。例如:
golang
代码解读
复制代码
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" 警告。这是一个尝试 go vet
检测机制的小练习。
不过,如果需要编写与同步原语(如 sync.Mutex
和 sync.WaitGroup
)相关的代码,noCopy
机制可能就会有用。
4. 其他 noCopy 策略
据我们了解,go vet
可以检测到未被严格禁止的潜在拷贝问题。有没有严格禁止拷贝的策略?是的,有。让我们看看 strings.Builder
的源代码:
golang
代码解读
复制代码
// 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 // External users should never get direct access to this buffer, // since the slice at some point will be converted to a string using unsafe, // also data between len(buf) and cap(buf) might be uninitialized. buf []byte } func (b *Builder) copyCheck() { if b.addr == nil { // This hack works around a failing of Go's escape analysis // that was causing b to escape and be heap allocated. // See issue 23382. // TODO: once issue 7921 is fixed, this should be reverted to // just "b.addr = b". b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b))) } else if b.addr != b { panic("strings: illegal use of non-zero Builder copied by value") } } // Write appends the contents of p to b's buffer. // Write always returns len(p), nil. func (b *Builder) Write(p []byte) (int, error) { b.copyCheck() b.buf = append(b.buf, p...) return len(p), nil }
关键点是:
golang
代码解读
复制代码
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))
这行代码的作用如下:
unsafe.Pointer(b)
:将b
转换为unsafe.Pointer
,以便与abi.NoEscape
一起使用。abi.NoEscape(unsafe.Pointer(b))
:告诉编译器b
不会转义,即可以继续在栈而不是堆上分配。(*Builder)(...)
: 将abi.NoEscape
返回值转换回*Builder
类型,以便正常使用。- 最后,
b.addr
被设置为b
本身的地址,这样可以防止Builder
被复制(在下面的逻辑中检查b.addr != b
)。
go1.23.0 builder.go abi.NoEscape
使用有拷贝行为的 strings.Builder
会导致 panic:
golang
代码解读
复制代码
func main() { var a strings.Builder a.Write([]byte("a")) b := a b.Write([]byte("b")) } // output panic: strings: illegal use of non-zero Builder copied by value goroutine 1 [running]: strings.(*Builder).copyCheck(...)
5. 总结
- 同步原语(如
sync.Mutex
和sync.WaitGroup
)不应被拷贝,因为一旦被拷贝,其内部状态就会重复,从而导致并发问题。 - 虽然 Go 本身并没有提供严格防止拷贝的机制,但
noCopy
结构提供了一种非严格的机制,用于go vet
工具的识别和拷贝检测。 - Go 中的某些源代码会在运行时执行 noCopy 检查并返回 panic,例如
strings.Builder
和sync.Cond
。