请考虑以下示例。假设我有一个在我的代码库中无处不在的对象:
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
替换为nil值(例如空字符串)。
防止这种情况发生的最佳方法是什么?有没有一种方法可以让golang在特定的结构初始化中不强制执行任何遗失的参数?有没有一种短毛绒可以检测到这种潜在的错误?
答案 0 :(得分:3)
我通常解决此问题的方法是仅使用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}
}
这迫使每个人都从同一位置获取实例。添加字段时,可以将其添加到函数定义中,然后在使用该字段的任何地方都会出现编译时错误。
答案 1 :(得分:2)
惯用的方法是完全不执行此操作,而是使用make the zero value useful。复制功能的示例并没有真正意义,因为它完全没有必要-您可以说:
copy := new(Person)
*copy = *origPerson
不需要专用功能,也不必保留最新的字段列表。如果您要为NewPerson
之类的新实例构造函数,则只需编写一个实例并使用它就可以了。 Linters在某些方面非常有用,但是没有什么能比公认的最佳实践和同行代码审查更好。
答案 2 :(得分:2)
首先,您的copyPerson()
函数不辜负其名称。它复制Person
的 some 个字段,但不是全部(必须)。它应该被命名为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
添加新字段,这将导致编译时错误,因为未加密的复合结构文字必须列出所有字段。同样,如果有人更改了可将新字段分配给旧字段的字段,这将无济于事(例如,有人将两个具有相同类型的字段彼此互换),并且不鼓励使用非键文字。
最好由包所有者在Person
类型定义旁边提供一个副本构造函数。因此,如果某人更改了Person
,则他/她应该负责保持CopyPerson()
仍在运行。就像其他人提到的那样,您应该已经有单元测试,如果CopyPerson()
不符合其名称,则该单元测试将失败。
如果您不能将CopyPerson()
放在Person
类型旁边,并请其作者来维护,请继续进行结构值复制以及对指针和标头字段的手动处理。 / p>
您可以创建person2
类型的Person
类型的快照。如果原始Person
类型更改,请使用空白的全局变量接收编译时警报,在这种情况下,包含源文件的copyPerson()
将拒绝编译,因此您将需要进行调整。< / p>
这是可以做到的:
type person2 struct {
Name string
Age int
}
var _ = Person(person2{})
如果Person
和person2
的字段不匹配,则空白变量声明将不会编译。
以上编译时检查的一种变体可能是使用类型化的nil
指针:
var _ = (*Person)((*person2)(nil))
答案 3 :(得分:1)
我不知道强制执行该规则的语言规则。
但是,您可以根据需要为Go vet编写自定义检查器。 Here's a recent post talking about that。
也就是说,我会在这里重新考虑设计。如果Person
结构在代码库中非常重要,请集中其创建和复制,以使“遥远的地方”不仅仅创建和移动Person
。重构代码,以便仅使用一个构造函数来构建Person
(也许像person.New
返回person.Person
之类的东西),然后就可以集中控制其字段的方式初始化。
答案 4 :(得分:0)
我能想到的最好的解决方案(不是很好)是定义一个与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
结构并希望具有此安全性的所有地方复制和粘贴结构定义。有什么更好的方法吗?
答案 5 :(得分:0)
这是我要怎么做:
func copyPerson(origPerson Person) *Person {
newPerson := origPerson
//proof that 'newPerson' points to a new person object
newPerson.name = "new name"
return &newPerson
}
答案 6 :(得分:0)
方法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:(不确定是否可行)
在使用反射的测试中可以覆盖吗?
如果我们比较原始结构中初始化的字段数(将所有字段初始化为具有不同于默认值的值)和复制函数返回的副本中的字段。