这是golang拾遗系列的第六篇。这个系列主要用来记录一些平时不常见的知识点,偶尔也会实现些有意思的小功能,比如这篇。
golang拾遗系列目录:
在本篇中我们将实现一个无法被复制的类型,顺便加深对引用类型、值传递以及指针的理解。
阅读本文前需要你拥有一定的前置知识,包括掌握基本的golang语法,能理解并应用接口,对sync包下的内容有粗略的了解。如果你准备好了,就可以接着往下看了。
本文索引
不考虑IDE提供的代码分析和go vet之类的静态分析工具,golang里几乎所有的类型都能被复制。
// 基本标量类型和指针
var i int = 1
iCopy := i
str := "string"
strCopy := str
pointer := &i
pointerCopy := pointer
iCopy2 := *pointer // 解引用后进行复制
// 结构体和数组
arr := [...]int{1, 2, 3}
arrCopy := arr
type Obj struct {
i int
}
obj := Obj{}
objCopy := obj
除了这些,golang还有函数和引用类型(slice、map、interface),这些类型也可以被复制,但稍有不同:
func f() {...}
f1 := f
f2 := f1
fmt.Println(f1, f2) // 0xabcdef 0xabcdef 打印出来的值是一样的
fmt.Println(&f1 == &f2) // false 虽然值一样,但确实是两个不同的变量
这里并没有真正复制处三份f的代码,f1和f2均指向f,f的代码始终只会有一份。map、slice和interface与之类似:
m := map[int]string{
0: "a",
1: "b",
}
mCopy := m // 两者引用同样的数据
mCopy[0] := "unknown"
m[0] == "unknown" // True
// slice的复制和map相同
interface是比较另类的,它的行为要分两种情况:
s := "string"
var i1 any = s
var i2 any = s
// 当把非指针和接口类型的值赋值给interface,会导致原来的对象被复制一份
s := "string"
var i1 any = s
var i2 any = i2
// 当把接口赋值给接口,底层引用的数据不会被复制,i1会复制s,i2此时和i1共有一个s的副本
ss := "string but pass by pointer"
var i3 any = &ss
var i4 any = i3
// i3和i4均引用ss,此时ss没有被复制,但指向ss的指针的值被复制了两次
上面的结果会一定程度上被编译优化干扰,比如少数情况下编译器可以确认赋值给接口的值从来没被修改并且生命周期不比源对象长,则可能不会进行复制。
所以这里有个小提示:如果要赋值给接口的数据比较大,那么最好以指针的形式赋值给接口,复制指针比复制大量的数据更高效。
从上一节可以看到,允许复制时会在某些情况下“闯祸”。比如:
sync.Mutex,复制一个锁并使用其副本会导致死锁显然在一些情况下禁止复制是合情合理的,这也是为什么我会写这篇文章。
但具体情况具体分析,不是说复制就是万恶之源,什么时候该支持复制,什么时候应该禁止,应该结合自己的实际情况。
想在别的语言禁止某个类型被复制,方法有很多,用c++举一例:
struct NoCopy {
NoCopy(const NoCopy &) = delete;
NoCopy &operator=(const NoCopy &) = delete;
};
可惜在golang里不支持这么做。
另外,因为golang没有运算符重载,所以很难在赋值的阶段就进行拦截,所以我们的侧重点在于“复制之后可以尽快检测到”。
所以我们先实现在对象被复制后报错的功能。虽然不如c++编译期就可以禁止复制那样优雅,但也算实现了功能,至少不什么都没有要强一些。
那么如何直到对象是否被复制了?很简单,看它的地址就行了,地址一样那必然是同一个对象,不一样了那说明复制出一个新的对象了。
顺着这个思路,我们需要一个机制来保存对象第一次创建时的地址,并在后续进行比较,于是第一版代码诞生了:
import "unsafe"
type noCopy struct {
p uintptr
}
func (nc *noCopy) check() {
if uintptr(unsafe.Pointer(nc)) != nc.p {
panic("copied")
}
}
逻辑比较清晰,每次调用check来检查当前的调用者的地址和保存地址是否相同,如果不同就panic。
为什么没有创建这个类型的方法?因为我们没法得知自己被其他类型创建时的地址,所以这块得让其他使用noCopy的类型代劳。
使用的时候需要把noCopy嵌入自己的struct,注意不能以指针的形式嵌入:
type SomethingCannotCopy struct {
noCopy
...
}
func (s *SomethingCannotCopy) DoWork() {
s.check()
fmt.Println("do something")
}
func NewSomethingCannotCopy() *SomethingCannotCopy {
s := &SomethingCannotCopy{
// 一些初始化
}
// 绑定地址
s.noCopy.p = unsafe.Pointer(&s.noCopy)
return s
}
注意初始化部分的代码,在这里我们需要把noCopy对象的地址绑定进去。现在可以实现运行时检测了:
func main() {
s1 := NewSomethingCannotCopy()
pointer := s1
s1Copy := *s1 // 这里实际上进行了复制,但需要调用方法的时候才能检测到
pointer.DoWork() // 正常打印出信息
s1Copy.DoWork() // panic
}
解释下原理:当SomethingCannotCopy被复制的时候,noCopy也会被复制,因此复制出来的noCopy的地址和原先的那个是不一样的,但他们内部记录的p是一样的,这样当被复制出来的noCopy对象调用check方法的时候就会触发panic。这也是为什么不要用指针形式嵌入它的原因。
功能实现了,但代码实在是太丑,而且耦合严重:只要用了noCopy,就必须在创建对象的同时初始化noCopy的实例,noCopy的初始化逻辑会侵入到其他对象的初始化逻辑中,这样的设计是不能接受的。
那么有没有更好的实现?答案是有的,而且在标准库里。
标准库的信号量sync.Cond是禁止复制的,而且比Mutex更为严格,因为复制它比复制锁更容易导致死锁和崩溃,所以标准库加上了运行时的动态检查。
主要代码如下:
type Cond struct {
// L is held while observing or changing the condition
L Locker
...
// 复制检查
checker copyChecker
}
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
func (c *Cond) Signal() {
// 检查自己是否被复制
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
checker实现了运行时检测是否被复制,但初始化的时候并不需要特殊处理这个checker,这是用了什么手法做到的呢?
看代码:
type copyChecker uintptr
func (c *copyChecker) check() {
if uintptr(*c) != uintptr(unsafe.Pointer(c)) && // step 1
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && // step 2
uintptr(*c) != uintptr(unsafe.Pointer(c)) { //step 3
panic("sync.Cond is copied")
}
}
看着很复杂,连原子操作都来了,这都是啥啊。但别怕,我给你捋一捋就明白了。
首先是checker初始化之后第一次调用:
step 1失败,进入step 2;step 2是False,判断流程直接结束;step 2是会失败的,这是要进入step 3;step 3再次比较c的值和它自己的地址是否相同,相同说明多个goroutine共用了一个checker,没有发生复制,所以检测通过不会panic。step 3的比较发现不相等,那么说明被复制了,直接panic然后我们再看其他情况下checker的流程:
step 1的结果是False,判断流程结束,不会panic;step 2,因为这里c的值不为0,所以表达式结果一定是True,所以进入step 3;step 3和step 1一样,结果是True,地址不同说明被复制,这时候if里面的语句会执行,因此panic。搞得这么麻烦,其实就是为了能干干净净地初始化。这样任何类型都只需要带上checker作为自己的字段就行,不用关心它是这么初始化的。
还有个小问题,为什么设置checker的值需要原子操作,但读取就不用呢?
因为读取一个uintptr的值,在现代的x86和arm处理器上只要一个指令,所以要么读到过时的值要么读到最新的值,不会读到错误的或者写了一半的不完整的值,对于读到旧值的情况(主要出现在第一次调用check的时候),还有step 3做进一步的检查,因此不会影响整个检测逻辑。而“比较并交换”显然一条指令做不完,如果在中间步骤被打断那么整个操作的结果很可能就是错的,从而影响整个检测逻辑,所以必须要用原子操作才行。
那么在读取的时候也使用atomic.Load行吗?当然行,但一是这么做仍然避免不了step 3的检测,可以思考下是为什么;二是原子操作相比直接读取会带来性能损失,在这里不使用原子操作也能保证正确性的情况下这是得不偿失的。
因为是运行时检测,所以我们得看看会对性能带来多少影响。我们使用改进版的checker。
type CheckBench struct {
num uint64
checker copyChecker
}
func (c *CheckBench) CheckCopy() {
c.checker.check()
c.num++
}
// 不进行检测
func (c *CheckBench) NoCheck() {
c.num++
}
func BenchmarkCheckBench_NoCheck(b *testing.B) {
c := CheckBench{}
for i := 0; i < b.N; i++ {
for j := 0; j < 50; j++ {
c.NoCheck()
}
}
}
func BenchmarkCheckBench_WithCheck(b *testing.B) {
c := CheckBench{}
for i := 0; i < b.N; i++ {
for j := 0; j < 50; j++ {
c.CheckCopy()
}
}
}
测试结果如下:
cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
BenchmarkCheckBench_NoCheck-8 17689137 68.36 ns/op
BenchmarkCheckBench_WithCheck-8 17563833 66.04 ns/op
几乎可以忽略不计,因为我们这里没有发生复制,所以几乎每次检测都是通过的,这对cpu的分支预测非常友好,所以性能损耗几乎可以忽略。
所以我们给cpu添点堵,让分支预测没那么容易:
func BenchmarkCheckBench_WithCheck(b *testing.B) {
for i := 0; i < b.N; i++ {
c := &CheckBench{}
for j := 0; j < 50; j++ {
c.CheckCopy()
}
}
}
func BenchmarkCheckBench_NoCheck(b *testing.B) {
for i := 0; i < b.N; i++ {
c := &CheckBench{}
for j := 0; j < 50; j++ {
c.NoCheck()
}
}
}
现在分支预测没那么容易了而且要多付出初始化时使用atomic的代价,测试结果会变成这样:
cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
BenchmarkCheckBench_WithCheck-8 15552717 74.84 ns/op
BenchmarkCheckBench_NoCheck-8 26441635 44.74 ns/op
差不多会慢40%。当然,实际的代码不会有这么极端,所以最坏可能也只会产生20%的影响,通常不太会成为性能瓶颈,运行时检测是否有影响还需结核profile。
优点:
缺点:
动态检测的缺点不少,能不能像c++那样编译期就禁止复制呢?
也可以,但得配合静态代码检测工具,比如自带的go vet。看下代码:
// 实现sync.Locker接口
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type SomethingCannotCopy struct {
noCopy
}
这样就行了,不需要再添加其他的代码。解释下原理:任何实现了sync.Locker的类型都不应该被拷贝,静态代码检测会检测出这些情况并报错。
所以类似下边的代码都是无法通过静态代码检测的:
func f(s SomethingCannotCopy) {
// 报错,因为参数会导致复制
// 返回SomethingCannotCopy也是不行的
}
func (s SomethingCannotCopy) Method() {
// 报错,因为非指针类型接收器会导致复制
}
func main() {
s := SomethingCannotCopy{}
sCopy := s // 报错
sInterface := any(s) // 报错
sPointer := &s // OK
sCopy2 := *sPointer // 报错
sInterface2 := any(sPointer) // OK
sCopy3 := *(sInterface2.(*SomethingCannotCopy)) // 报错
}
基本上涵盖了所以会产生复制操作的地方,基本能在编译期完成检测。
如果跳过go vet,直接使用go run或者go build,那么上面的代码可以正常编译并运行。
因为只有静态检测,因此没有什么运行时开销,所以性能这节就不需要费笔墨了。主要来看下这种方案的优缺点。
优点:
缺点:
sync.Locker,然而很多时候我们的类型并不是类似锁的资源,使用这个接口只是为了静态检测,这会带来代码被误用的风险标准库也使用的这套方案,建议仔细阅读这个issue里的讨论。
看过运行时检测和静态检测两种方案之后,我们会发现这些做法多少都有些问题,不尽如人意。
所以我们还是要追求一种更好用的,更符合golang风格的做法。幸运的是,这样的做法是存在的。
首先我们创建一个worker包,里面定义一个Worker接口,包中的数据对外以Worker接口的形式提供:
package worker
import (
"fmt"
)
// 对外只提供接口来访问数据
type Worker interface {
Work()
}
// 内部类型不导出,以接口的形式供外部使用
type normalWorker struct {
// data members
}
func (*normalWorker) Work() {
fmt.Println("I am a normal worker.")
}
func NewNormalWorker() Worker {
return &normalWorker{}
}
type specialWorker struct {
// data members
}
func (*specialWorker) Work() {
fmt.Println("I am a special worker.")
}
func NewSpecialWorker() Worker {
return &specialWorker{}
}
worker包对外只提供Worker接口,用户可以使用NewNormalWorker和NewSpecialWorker来生成不同种类的worker,用户不需要关心具体的返回类型,只要使用得到的Worker接口即可。
这么做的话,在worker包之外是看不到normalWorker和specialWorker这两个类型的,所以没法靠反射和类型断言取出接口引用的数据;因为我们传给接口的是指针,因此源数据不会被复制;同时我们在第一节提到过,把一个接口赋值给另一个接口(worker包之外你只能这么做),底层被引用的数据不会被复制,因此在包外始终不会在这两个类型上产生复制的行为。
因此下面这样的代码是不可能通过编译的:
func main() {
w := worker.NewSpecialWorker()
// worker.specialWorker 在worker包以外不可见,因此编译错误
wCopy := *(w.(*worker.specialWorker))
wCopy.Work()
}
这样就实现了worker包之外的禁止复制,下面来看看优缺点。
优点:
缺点:
综合来说,这种方案是实现成本最低的。
现在我们有三种方式防止我们的类型被复制:
一共三种方案,选择困难症仿佛要发作了。别着急,我们一起看看标准库是怎么做的:
sync.Cond同时使用了方案一和方案二,因为设计者确实很不希望条件变量被复制sync.Mutex、sync.Pool和sync.WaitGroup使用了方案二,需要配合go vetcrypto包下的那些Hash和Cipher确实没什么意义会带来误用,正好借着方案三避免了这些问题综合来看首选的应该是方案三;但也有需要使用方案二的时候,比如sync包中的那些同步机构;使用最少的是方案一,尽可能地不要设计出类似的代码。
还有一点需要注意,如果你的类型里有字段是sync.Pool、sync.WaitGroup、sync.RWMutex、sync.Mutex、sync.Cond、sync.Map或sync.Once,那么这个类型本身也是不可复制的,也不需要额外实现禁止复制的功能,因为那些字段自带了。
最后,我只想说golang的语言技能实在是太简陋了,想只依赖语言特性实现禁止复制的功能不太现实,更多的还是需要靠“设计”。
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳