写点什么

5 分钟搞懂 Golang noCopy 策略

作者:俞凡
  • 2024-12-27
    上海
  • 本文字数:3538 字

    阅读完需:约 12 分钟

本文介绍了 Golang 中的 noCopy 策略,解释了如何防止包含锁的结构体被错误拷贝,以及如何使用 go vet 工具检测潜在的拷贝问题。原文:noCopy Strategies You Should Know in Golang


1. Sync.noCopy

在学习 Go 的 WaitGroup 代码时,我注意到了 noCopy,并看到一个熟悉的注释:"首次使用后不得复制"。


// 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 一起出现。



// 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 类型实现了两种方法:LockUnlock,这两种方法都是非操作方法。

  • 注释强调,LockUnlockgo vet 检查器使用。


noCopy 类型没有实际的功能特性,只有通过思索和实验才能理解其具体用途,以及为什么 "首次使用后不得复制"?

2. Go Vet 和 "锁值错误传递"

当我们输入以下命令:


go tool vet help copylocks
复制代码


输出:


copylocks: check for locks erroneously passed by value
Inadvertently copying a value containing a lock, such as sync.Mutex orsync.WaitGroup, may cause both copies to malfunction. Generally suchvalues should be referred to through a pointer.
复制代码


Go Vet 告诉我们在使用包含锁(如 sync.Mutexsync.WaitGroup)的值并通过值传递时,可能会导致意想不到的问题。例如:


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")}
复制代码


运行这段代码,将输出错误信息:


// outputtestfatal 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.MutexCopy
复制代码


错误原因是 LockUnlock 方法使用了值接收器 t,在调用方法时会创建 T 的副本,这意味着 Unlock 中的锁实例与 Lock 中的锁实例不匹配。


为了解决这个问题,可以将接收器改为指针类型:


func (t *T) Lock() {  t.lock.Lock()}
func (t *T) Unlock() { t.lock.Unlock()}
复制代码


同样,在使用 CondWaitGroup 和其他包含锁的类型时,需要确保它们在首次使用后不会被复制。例如:


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!")}
复制代码


运行这段代码,也会输出错误信息:


/////Worker 3 startingWorker 1 startingWorker 2 startingWorker 1 doneWorker 3 doneWorker 2 donefatal 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 vetnoCopy 机制是一种防止结构体被拷贝的方法,尤其是那些包含同步原语(如 sync.Mutexsync.WaitGroup)的结构,目的是防止意外的锁拷贝,但这种防止并不是强制性的,是否拷贝需要由开发者检测。例如:


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.Mutexsync.WaitGroup)相关的代码,noCopy 机制可能就会有用。

4. 其他 noCopy 策略

据我们了解,go vet 可以检测到未被严格禁止的潜在拷贝问题。有没有严格禁止拷贝的策略?是的,有。让我们看看 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
// 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}
复制代码


关键点是:


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:


func main() {    var a strings.Builder    a.Write([]byte("a"))    b := a    b.Write([]byte("b"))}// outputpanic: strings: illegal use of non-zero Builder copied by valuegoroutine 1 [running]:strings.(*Builder).copyCheck(...)
复制代码
5. 总结
  • 同步原语(如 sync.Mutexsync.WaitGroup)不应被拷贝,因为一旦被拷贝,其内部状态就会重复,从而导致并发问题。

  • 虽然 Go 本身并没有提供严格防止拷贝的机制,但 noCopy 结构提供了一种非严格的机制,用于 go vet 工具的识别和拷贝检测。

  • Go 中的某些源代码会在运行时执行 noCopy 检查并返回 panic,例如 strings.Buildersync.Cond

参考资料

Detect locks passed by value in Go


What does “nocopy after first use” mean in golang and how




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
5分钟搞懂 Golang noCopy策略_golang_俞凡_InfoQ写作社区