背景

最近看 Go 标准库源码时经常遇到禁止拷贝对象的使用场景,比如当我们使用 strings.Builder 或者 sync.Pool 对象的时候会被禁止拷贝,这是如何实现的呢?

主要有以下两种方式:

方式一:手动检查

这种需要我们在运行时通过 copyCheck 方法来检查是否发生了拷贝操作,我们看下 strings.Builder 的实现方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 结构体
type Builder struct {
	addr *Builder   // addr 保存了 Builder 对象的地址
	buf  []byte
}

func (b *Builder) copyCheck() {
	if b.addr == nil {      // 首次调用 copyCheck 方法时保存自身地址
		b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
	} else if b.addr != b { // 非首次调用 copyCheck 方法时比较当前对象的地址是否与 addr 相等,不相等则发生 panic
		panic("strings: illegal use of non-zero Builder copied by value")
	}
}

//执行写方法时,调用 copyCheck 进行拷贝检查
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck() 
    // ...
}

需要注意的是,strings.Builder 只有在每次执行写方法时才会调用 copyCheck 进行拷贝检查,所以在调用任何写入方法之前是可以进行拷贝的,此时还未进行检查。

关于 strings.Builder的源码分析,可以点击链接查看。

方式二:noCopy

noCopy 是 go1.7 开始引入的一个静态检查机制,它不仅仅工作在运行时或标准库,同时也对用户代码有效。

我们看下标准库 sync.Pool 是如何使用 noCopy 实现禁止拷贝操作的:

1
2
3
4
type Pool struct {
    noCopy noCopy
    //忽略其他字段
}

因为 Pool 不希望被复制,所以结构体里有一个 noCopy 的字段,使用 go vet 工具可以检测到用户代码是否复制了 Pool。

noCopy 是如何实现的呢?

1
2
3
4
5
6
7
8
9
// noCopy 用于嵌入一个结构体中来保证其第一次使用后不会被复制
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

实现非常简单,用户只需实现这样的不消耗内存、仅用于静态分析的结构,来保证一个对象在第一次使用后不会发生复制。

我们做个测试看下效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type noCopy struct{}
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type User struct {
	noCopy
	Name string
}

func main() {
	u := User{Name: "maratrix"}
	fmt.Println(u) //这里发生了拷贝
}

$ go vet main.go
$ ./main.go:17:14: call of fmt.Println copies lock value: command-line-arguments.User

可以看出,使用 go vet 检查时会发生错误。 那能否可以正常编译运行呢?我们试一下:

1
2
$ go run main.go
$ {{} maratrix}

编译正常。

所以这里要注意:noCopy 只有在使用 go vet 检查才会报错,但仍可以正常编译运行。

小结

主要对笔者最近阅读 Go 源码时遇到的两种禁止对象拷贝的实现方式做个记录总结:

  • 手动实现,需要在方法中手动触发 copyCheck 方法,发生拷贝会 panic。
  • noCopy,通过在结构体重引入 noCopy,然后使用 go vet 进行检查,发生拷贝仍可以正常编译。

参考

深度解密 Go 语言之 sync.pool

标准库 sync.noCopy 源码

标准库 strings.Builder 源码

标准库 sync.Pool 源码