问题描述
我维护一个库,其中导出的结构具有不应浅拷贝的字段。例如:
type Example struct {
Val int
Nums []int
}
由于 Nums
是切片类型的字段,因此 Foo
实例的浅拷贝复制了切片头并允许存在错误:
foo := Example{Val: 1,Nums: []int{100}}
bar := Example
bar.Nums[0] = 200
fmt.Println(foo.Nums) // [200]
- 返回指针的构造函数,但无论如何都不能阻止客户端取消引用和浅拷贝:
pfoo := lib.NewFoo() // returns type `*Foo`
foo := *pfoo // Now variable foo is type `Foo`
foo2 := foo // I don't want this
- 向库中添加丰富的文档以阻止浅拷贝。我还可以声明我的库中所有使用
Foo
来要求指针类型的方法,以确保在我这边的安全,但同样,导入程序代码可能会编写使用 {{1} 的函数}} 值。 - 向
Foo
添加Clone()
方法,但这属于“文档”问题:人们可能不会阅读它。 - 向
Foo
添加自定义检查,但这不会导致编译器错误。 - 重写库以避免暴露不可复制的字段
- 忍受可能出现错误
解决方法
问题 runtime: add NoCopy documentation struct type? 解决了这个问题。
comment on the issue 推荐此解决方案:
请注意,绝对必须选择加入 vet 检查的代码已经可以这样做。一个包可以定义:
type noCopy struct{}
func (*noCopy) Lock() {}
然后将 noCopy noCopy 放入任何必须由 vet 标记的结构中。
还需要 Unlock 方法来触发警告。以下是示例的完整解决方案:
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type Example struct {
noCopy noCopy
Val int
Nums []int
}
通过此更改,go vet
命令会为以下代码打印警告 assignment copies lock value to y:Example contains noCopy
:
var x Example
y := x
Run the example on the playground。
记录不应复制值的要求。如果应用程序需要深层复制,请提供 Clone()
方法。
该方法不会增加 Example
的大小,因为 struct{}
的大小为零。
标准库 sync.WaitGroup
类型 uses this approach。
强制编译器错误的可移植方法是依赖语言规范,这在这种情况下没有多大帮助。
其他解决方案可以是依赖于实现的,也可以是静态分析。这是对我研究内容的解释:
使用 go vet
字段强制发出 noCopy
警告
可以强制编译器错误的情况的实用性非常有限,一般不会阻止浅拷贝。不幸的是,规格在这里没有提供任何其他有用的帮助。
另一种选择是依靠 go vet
检查 copylocks
来报告不希望使用的结构,例如具有 sync.Locker
接口实现的赋值。
要触发此警告,您只需在结构中添加一个 sync.Mutex
字段:
type Foo struct {
m sync.Mutex
}
func main() {
foo := Foo{}
foo2 := foo // assignment copies lock value to foo2: play.Foo contains sync.Mutex
}
如果您不想使用不必要的字段增加结构体内存占用,您可以定义自己的实现 sync.Locker
的类型:
// implements sync.Locker
type noCopy struct {}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type Foo struct {
_ noCopy
}
不要嵌入 noCopy
,将其声明为命名字段。即使该字段未导出,嵌入它也会将导出的 Lock()
和 Unlock()
方法提升到您的结构中。
强制恐慌(不推荐)
标准库 strings.Builder
带有一个 hack,如果在使用结构后尝试写入,它会发生恐慌:
//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
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)(noescape(unsafe.Pointer(b)))
} else 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()
// ...
}
copyCheck
方法在 Builder
结构上发生写访问时使用。在第一次访问时,当 Builder
不再为零值时,该方法将当前指针存储在结构本身中。在随后的每次写访问中,代码都会验证当前接收器是否仍然相同。
请注意,您可以在类型为 *T
的非指针变量上调用带有指针接收器 T
的方法:
如果 x
是可寻址的并且 &x
的方法集包含 m
,则 x.m()
是 (&x).m()
的简写
因此在完成以下任务后:
var bld strings.Builder
bld.Write(/* ... */)
copied := bld
调用 copied.Write
等于 (&copied).Write()
,这将具有不同的指针接收器,从而使 b.addr != b
为真。
这看起来是一种防止浅拷贝的简单方法,即使让您的导入程序在运行时恐慌可能是一个更糟糕的用户体验。然而,issue 7921 导致的逃逸分析黑客是不安全的,可能会破坏交易。
在该问题解决并恢复为代码后:
if b.addr == nil {
b.addr = b
} else if b.addr != b {
// panic
}
恐慌复制可能成为一个可行的选择。