编程 Golang 中你应该知道的 noCopy 策略

2024-11-19 05:40:53 +0800 CST views 548

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 的定义,我们可以得出以下几点:

  1. noCopy 类型是空的结构体。
  2. noCopy 类型实现了两个方法:LockUnlock,而且都是空方法(即 no-op)。
  3. 注释中强调了 LockUnlockgo 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")
}

此代码会输出错误,因为 LockUnlock 方法使用了值接收者,导致 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.Mutexsync.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 总结

  1. 同步原语(如 sync.Mutexsync.WaitGroup)不应被拷贝,否则会引发并发问题。
  2. noCopy 提供了一种非强制的防拷贝机制,通过 go vet 工具进行静态检测。
  3. 某些 Go 源代码会在运行时进行更严格的检查,例如 strings.Buildersync.Cond,一旦检测到拷贝行为,将触发 panic
复制全文 生成海报 Golang 并发 编程技巧

推荐文章

10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
避免 Go 语言中的接口污染
2024-11-19 05:20:53 +0800 CST
Golang 随机公平库 satmihir/fair
2024-11-19 03:28:37 +0800 CST
Rust 并发执行异步操作
2024-11-19 08:16:42 +0800 CST
Go 语言实现 API 限流的最佳实践
2024-11-19 01:51:21 +0800 CST
PHP 唯一卡号生成
2024-11-18 21:24:12 +0800 CST
Vue中的样式绑定是如何实现的?
2024-11-18 10:52:14 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
介绍Vue3的静态提升是什么?
2024-11-18 10:25:10 +0800 CST
如何在Vue中处理动态路由?
2024-11-19 06:09:50 +0800 CST
15 个 JavaScript 性能优化技巧
2024-11-19 07:52:10 +0800 CST
淘宝npm镜像使用方法
2024-11-18 23:50:48 +0800 CST
使用Python提取图片中的GPS信息
2024-11-18 13:46:22 +0800 CST
解决python “No module named pip”
2024-11-18 11:49:18 +0800 CST
H5保险购买与投诉意见
2024-11-19 03:48:35 +0800 CST
Go的父子类的简单使用
2024-11-18 14:56:32 +0800 CST
使用 sync.Pool 优化 Go 程序性能
2024-11-19 05:56:51 +0800 CST
ElasticSearch集群搭建指南
2024-11-19 02:31:21 +0800 CST
程序员茄子在线接单