考虑下面这个例子。比方说,我有这样的对象,它是我的整个代码库无处不在:
type Person struct {
Name string
Age int
[some other fields]
}
在代码库某处深,我也有,创建一个新的Person
结构的一些代码。也许是类似如下效用函数:
func copyPerson(origPerson Person) *Person {
copy := Person{
Name: origPerson.Name,
Age: origPerson.Age,
[some other fields]
}
return ©
}
另一位开发人员走来,并增加了一个新的领域Gender
到Person
结构。然而,由于copyPerson
功能是在遥远的一段代码,他们忘记更新copyPerson
。因为如果省略创建一个结构时,参数golang不会引发任何警告或错误,该代码将编译并出现做工精细;唯一的区别是,该方法copyPerson
现在将故障转移Gender
结构的复制,和copyPerson
的结果将有Gender
具有零值(例如空字符串)所取代。
什么是防止这种情况发生的最好方法是什么?有没有办法问golang执行特定结构的初始化没有缺少的参数?有没有可以检测到这种类型的潜在错误的棉绒?
我通常解决这个问题的方法是只使用NewPerson(params)
,而不是出口的人,而是一个接口。
package person
// Exporting interface instead of struct
type Person interface {
GetName() string
}
// Struct is not exported
type person struct {
Name string
Age int
Gender bool
}
// We are forced to call the constructor to get an instance of person
func New(name string, age int, gender bool) Person {
return person{name, age, gender}
}
这迫使每个人都来自同一个地方获得一个实例。当你添加一个字段,可以将其添加到函数定义,然后你随处编译它的使用时间的错误。
惯用的方法是无法做到这一点在所有的,而是make the zero value useful。复印功能的例子并没有真正意义,因为它是完全没有必要的 - 你可以只说:
copy := new(Person)
*copy = *origPerson
而不需要专门的功能,也必须保持字段的列表保持最新状态。如果你想要像NewPerson
新实例构造函数,只写一个,并把它作为理所当然的事。棉短绒是伟大的一些事情,但没有什么比充分理解的最佳实践和同行代码审查。
首先,你copyPerson()
功能不辜负它的名字。它拷贝Person
的某些领域,但不是(必然)所有。它应该已经被命名为copySomeFieldsOfPerson()
。
要复制一个完整的结构值,只是分配结构值。如果你有一个函数接收非指针Person
,这已经是一个副本,因此只返回其地址:
func copyPerson(p Person) *Person {
return &p
}
这一切,这将复制Person
的现在和未来所有领域。
现在可能存在字段的指针或头状值(如切片),这应该是从原来的领域(更精确地从尖锐的物体),在这种情况下,你需要进行手动调整,例如“分离”的情况下
type Person struct {
Name string
Age int
Data []byte
}
func copyPerson(p Person) *Person {
p2 := p
p2.Data = append(p2.Data, p.Data...)
return &p2
}
或替代方案不使p
的另一个副本,但仍脱离Person.Data
:
func copyPerson(p Person) *Person {
var data []byte
p.Data = append(data, p.Data...)
return &p
}
当然,如果有人添加一个字段,也需要手工操作,这不会帮助你。
你也可以使用无锁的文字,就像这样:
func copyPerson(p Person) *Person {
return &Person{
p.Name,
p.Age,
}
}
这将导致一个编译时错误,如果有人添加了一个新的领域Person
,因为unkeyed的复合结构字面必须列出所有字段。同样,如果有人更改其中新字段分配给旧的(例如有人掉期2场旁边彼此具有相同类型)的字段这不会帮助你,也无锁文字都望而却步。
最好将是包所有者提供一个拷贝构造函数,旁边Person
类型定义。因此,如果有人改变Person
,他/她应该负责保管CopyPerson()
仍在运行。正如其他人所说,你应该已经有单元测试,如果CopyPerson()
不辜负它的名字应该失败。
如果你不能把旁边CopyPerson()
类型Person
和具有它的作者维护它,继续与指针的结构值复制和人工搬运和头般的领域。
你可以创建一个person2
型这是一种在Person
类型的“快照”。使用空白全局变量接受编译时警告,如果原来的Person
类型的变化,其中包括源文件的情况下copyPerson()
的将拒绝编译,所以你就会知道它需要调整。
这是如何做到:
type person2 struct {
Name string
Age int
}
var _ = Person(person2{})
如果Person
和person2
的领域不匹配空白变量声明不会编译。
上述编译时检查的变化可能是使用类型化nil
指针:
var _ = (*Person)((*person2)(nil))
我不知道,强制执行语言规则。
但是你可以编写自定义跳棋为Go vet,如果你愿意的话。 Here's a recent post talking about that。
这就是说,我会在这里重新设计。如果Person
结构是在你的代码库非常重要的,它集中创建和复制,使“遥远的地方”不只是创建和移动Person
s。重构代码,以便只有一个构造函数是用来建立Person
s(可能像person.New
返回person.Person
),然后你就可以集中控制其字段如何初始化。
我已经能够拿出(这不是很好),最好的解决办法是定义一个新的结构tempPerson
等同于Person
结构,并把它就近用来初始化一个新的Person结构的任何代码,并更改代码初始化Person
,使其代替初始化它作为一个tempPerson
但随后其强制转换为Person
。像这样:
type tempPerson struct {
Name string
Age int
[some other fields]
}
func copyPerson(origPerson Person) *Person {
tempCopy := tempPerson{
Name: orig.Name,
Age: orig.Age,
[some other fields]
}
copy := (Person)(tempCopy)
return ©
}
如果另一场Gender
被添加到Person
但不tempPerson
代码将在编译时失败这样。据推测,开发者将后来看到错误,编辑tempPerson
,以满足他们改变Person
,并在此过程中发现它使用tempPerson
附近的代码,并认识到他们应该修改该代码同时处理Gender
领域也是如此。
因为它涉及复制和粘贴无处不在的结构定义,我们初始化一个Person
结构,并希望有这样的安全,我不喜欢这种解决方案。有没有更好的办法?
下面是我会做:
func copyPerson(origPerson Person) *Person {
newPerson := origPerson
//proof that 'newPerson' points to a new person object
newPerson.name = "new name"
return &newPerson
}
方法1添加类似的拷贝构造函数:
type Person struct {
Name string
Age int
}
func CopyPerson(name string, age int)(*Person, error){
// check params passed if needed
return &Person{Name: name, Age: age}, nil
}
p := CopyPerson(p1.Name, p1.age) // force all fields to be passed
方法2:(不知道这是可能的)
这可以在测试中被覆盖说使用反射? 如果我们比较初始化的字段数(初始化所有的字段值比默认值不同),在原来的结构和复制功能回到在副本中的字段。