转载 | 防止在 Go 中意外复制结构
默认情况下,Go 语言在传递值时会进行复制。但有时候这种复制并不理想。例如,如果你不小心复制了一个互斥锁(mutex
),并且多个 goroutine
分别使用复制出来的锁实例,那么它们之间就不能正确同步。在这种情况下,传递指向锁的指针可以避免复制,从而按预期工作。
举个例子:按值传递一个 sync.WaitGroup
会以微妙的方式导致问题发生:
func f(wg sync.WaitGroup)
func main()
sync.WaitGroup
让你等待多个 goroutine
完成一些工作。在其内部,它是一个带有 Add
、Done
和 Wait
等方法的结构体,用于同步并发运行的 goroutine
。
这段代码虽然可以正常编译,但会导致错误的行为,因为我们在 f
函数中复制了锁,而不是引用它。
幸运的是,go vet
工具能够检测到这个问题。如果你对这段代码运行 go vet
,你会收到类似如下的警告:
这意味着我们错误地按值传递了 wg
,而实际上应该传递它的引用。下面是修正方法:
func f(wg *sync.WaitGroup)
func main()
由于这种错误的复制不会导致编译时错误,如果你跳过了 go vet
,可能永远也无法发现它。这也是你应该始终使用 go vet
进行代码检查的另一个原因。
我很好奇 Go 工具链是如何强制执行这一点的。线索就在 vet
的警告中:
所以,sync.WaitGroup
里面的 sync.noCopy
结构体在你按值传递时会做一些操作,以提醒 go vet
。
查看 sync.WaitGroup
的实现,你会看到:
type WaitGroup struct
然后我追踪了 noCopy
在 sync/cond.go
中的定义。
// noCopy may be added to structs which must not be copied
// after the first use.
// 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 ()
func ()
只要在 noCopy
上定义空操作的 Lock
和 Unlock
方法就足够了。这实现了 Locker
接口。然后,如果你把这个结构体放到另一个结构体里,go vet
会标记那些试图复制外层结构体的情况。
另外,注意注释:不要将 noCopy
嵌入(embed)进去,而是要显式地包含它。嵌入会将 Lock
和 Unlock
方法暴露到外层结构体上,而这通常不是你想要的。
Go 工具链通过 -copylocks
检查器来强制执行这一点。它是 go vet
的一部分。你可以用 go vet -copylocks ./...
专门调用它。它会查找任何嵌套了带有 Lock
和 Unlock
方法结构体的值复制情况。不管那些方法实际做什么,只要有就足够了。
运行 vet
时,它会遍历抽象语法树(AST
),并对赋值、函数调用、返回值、结构字面量、range
循环、channel
发送等几乎所有值可能被复制的地方应用这个检查器。如果发现你复制了带有 noCopy
的结构体,它就会警告你。你可以在这里看到检查实现。
有趣的是,如果你定义的 noCopy
不是一个结构体,但实现了 Locker
接口,vet 会忽略它。我在 Go 1.24 上测试过这一点。
type noCopy int // this is valid but vet doesn't get triggered
func ()
func ()
这不会触发 vet
警告。只有当 noCopy
是结构体时才有效。原因是 vet
在检查何时触发警告时采用了一个捷径。它目前明确地只查找满足 Locker
接口的结构体类型,即使其他类型也实现了该接口,vet
也会忽略。
你在 sync
包的其他部分也会看到这种做法。sync.Mutex
就用了同样的技巧:
type Mutex struct
与 sync.Once
相同:
type Once struct
下面是一个完整的示例,展示如何滥用 -copylocks
来防止复制我们自己的结构体:
type Svc struct
type noCopy struct
func ()
func ()
// Use this
func main()
运行 go vet
会得到以下结果:
assignment copies lock value to s: play.Svc contains play.noCopy
call of fmt.Println copies lock value: play.Svc contains play.noCopy
有人在 Reddit 上问我,究竟是什么触发了 go vet
中 copylock
检查器——是结构体字面量的名字 noCopy
,还是它实现了 Locker
接口?
名字 noCopy
并没有特别的意义。你可以随意命名它。只要它实现了 Locker
接口,如果包含它的结构体被复制,go vet
就会发出警告。详见这个 Go Playground 代码片段。