防止在结构初始化中丢失字段
Prevent missing fields in struct initialization
考虑这个例子。假设我有一个在我的代码库中无处不在的对象:
type Person struct {
Name string
Age int
[some other fields]
}
在代码库深处的某个地方,我还有一些创建新 Person
结构的代码。也许它类似于下面的实用函数(请注意,这只是一些创建 Person
的函数的示例——我的问题的重点不是专门询问复制函数):
func copyPerson(origPerson Person) *Person {
copy := Person{
Name: origPerson.Name,
Age: origPerson.Age,
[some other fields]
}
return ©
}
另一位开发人员出现并向 Person
结构添加了一个新字段 Gender
。然而,由于 copyPerson
函数在一段遥远的代码中,他们忘记更新 copyPerson
。如果您在创建结构时省略参数,golang 不会抛出任何警告或错误,代码将编译并看起来工作正常;唯一的区别是 copyPerson
方法现在无法复制 Gender
结构,并且 copyPerson
的结果将 Gender
替换为 nil 值(例如空字符串)。
防止这种情况发生的最佳方法是什么?有没有办法让 golang 在特定的结构初始化中强制执行不丢失参数?是否有可以检测此类潜在错误的 linter?
我能想到的最好的解决方案(不是很好)是定义一个与 Person
结构相同的新结构 tempPerson
并将其放在任何代码附近它会初始化一个新的 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
结构并希望获得这种安全性的任何地方复制和粘贴结构定义。有没有更好的办法?
我不知道强制执行此操作的语言规则。
但是您可以为 Go vet if you'd like. Here's a recent post talking about that 编写自定义检查程序。
也就是说,我会重新考虑这里的设计。如果 Person
结构在您的代码库中如此重要,请集中其创建和复制,以便 "distant places" 不只是创建和移动 Person
。重构您的代码,以便仅使用一个构造函数来构建 Person
s(可能类似于 person.New
返回 person.Person
),然后您将能够集中控制其字段的方式已初始化。
这是我的做法:
func copyPerson(origPerson Person) *Person {
newPerson := origPerson
//proof that 'newPerson' points to a new person object
newPerson.name = "new name"
return &newPerson
}
我要解决这个问题的方法是只使用 NewPerson(params)
而不是导出人。这使得获得 person
实例的唯一方法是通过您的 New
方法。
package person
// 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 添加类似复制构造函数的内容:
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
方法二:(不确定是否可行)
这可以在使用反射的测试中涵盖吗?
如果我们比较原始结构中初始化的字段数(使用不同于默认值的值初始化所有字段)和复制函数返回的副本中的字段。
惯用的方法是根本不这样做,而是 make the zero value useful。复制函数的例子没有意义,因为它完全没有必要——你可以说:
copy := new(Person)
*copy = *origPerson
并且不需要专门的功能,也不必使字段列表保持最新。如果你想要像NewPerson
这样的新实例的构造函数,那么写一个并使用它是理所当然的。 Linters 在某些方面非常有用,但没有什么能比得上广为人知的最佳实践和同行代码审查。
首先,您的copyPerson()
函数名不副实。它复制 Person
的 一些 字段,但不是(必须)全部。它应该被命名为 copySomeFieldsOfPerson()
.
要复制完整的结构值,只需分配结构值即可。如果你有一个接收 non-pointer Person
的函数,它已经是一个副本,所以只需 return 它的地址:
func copyPerson(p Person) *Person {
return &p
}
就是这样,这将复制 Person
的所有当前和未来字段。
现在可能存在字段是指针或 header-like 值(如切片)的情况,它们应该是来自原始字段的 "detached"(更准确地说是来自指向的 object) ,在这种情况下,您确实需要进行手动调整,例如
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
添加新字段,这将导致 compile-time 错误,因为未键控的复合结构文字必须列出所有字段。同样,如果有人更改了可将新字段分配给旧字段的字段(例如,有人将具有相同类型的 2 个彼此相邻的字段交换),这将无济于事,也不鼓励未加密的文字。
包所有者最好在 Person
类型定义旁边提供一个复制构造函数。因此,如果有人更改 Person
,他/她应该负责保持 CopyPerson()
仍然可操作。正如其他人提到的,你应该已经有了单元测试,如果 CopyPerson()
不符合它的名字,它应该会失败。
最佳可行方案?
如果您不能将 CopyPerson()
放在 Person
类型旁边并让其作者维护它,请继续进行指针和 [=72= 的结构值复制和手动处理] 字段。
并且您可以创建 person2
类型,它是 Person
类型的 "snapshot"。如果原始 Person
类型更改,请使用空白全局变量接收 compile-time 警报,在这种情况下 copyPerson()
包含的源文件将拒绝编译,因此您将知道它需要调整.
这是如何完成的:
type person2 struct {
Name string
Age int
}
var _ = Person(person2{})
如果 Person
和 person2
的字段不匹配,则空白变量声明将不会编译。
上述 compile-time 检查的变体可能是使用类型化的-nil
指针:
var _ = (*Person)((*person2)(nil))
考虑这个例子。假设我有一个在我的代码库中无处不在的对象:
type Person struct {
Name string
Age int
[some other fields]
}
在代码库深处的某个地方,我还有一些创建新 Person
结构的代码。也许它类似于下面的实用函数(请注意,这只是一些创建 Person
的函数的示例——我的问题的重点不是专门询问复制函数):
func copyPerson(origPerson Person) *Person {
copy := Person{
Name: origPerson.Name,
Age: origPerson.Age,
[some other fields]
}
return ©
}
另一位开发人员出现并向 Person
结构添加了一个新字段 Gender
。然而,由于 copyPerson
函数在一段遥远的代码中,他们忘记更新 copyPerson
。如果您在创建结构时省略参数,golang 不会抛出任何警告或错误,代码将编译并看起来工作正常;唯一的区别是 copyPerson
方法现在无法复制 Gender
结构,并且 copyPerson
的结果将 Gender
替换为 nil 值(例如空字符串)。
防止这种情况发生的最佳方法是什么?有没有办法让 golang 在特定的结构初始化中强制执行不丢失参数?是否有可以检测此类潜在错误的 linter?
我能想到的最好的解决方案(不是很好)是定义一个与 Person
结构相同的新结构 tempPerson
并将其放在任何代码附近它会初始化一个新的 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
结构并希望获得这种安全性的任何地方复制和粘贴结构定义。有没有更好的办法?
我不知道强制执行此操作的语言规则。
但是您可以为 Go vet if you'd like. Here's a recent post talking about that 编写自定义检查程序。
也就是说,我会重新考虑这里的设计。如果 Person
结构在您的代码库中如此重要,请集中其创建和复制,以便 "distant places" 不只是创建和移动 Person
。重构您的代码,以便仅使用一个构造函数来构建 Person
s(可能类似于 person.New
返回 person.Person
),然后您将能够集中控制其字段的方式已初始化。
这是我的做法:
func copyPerson(origPerson Person) *Person {
newPerson := origPerson
//proof that 'newPerson' points to a new person object
newPerson.name = "new name"
return &newPerson
}
我要解决这个问题的方法是只使用 NewPerson(params)
而不是导出人。这使得获得 person
实例的唯一方法是通过您的 New
方法。
package person
// 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 添加类似复制构造函数的内容:
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
方法二:(不确定是否可行)
这可以在使用反射的测试中涵盖吗?
如果我们比较原始结构中初始化的字段数(使用不同于默认值的值初始化所有字段)和复制函数返回的副本中的字段。
惯用的方法是根本不这样做,而是 make the zero value useful。复制函数的例子没有意义,因为它完全没有必要——你可以说:
copy := new(Person)
*copy = *origPerson
并且不需要专门的功能,也不必使字段列表保持最新。如果你想要像NewPerson
这样的新实例的构造函数,那么写一个并使用它是理所当然的。 Linters 在某些方面非常有用,但没有什么能比得上广为人知的最佳实践和同行代码审查。
首先,您的copyPerson()
函数名不副实。它复制 Person
的 一些 字段,但不是(必须)全部。它应该被命名为 copySomeFieldsOfPerson()
.
要复制完整的结构值,只需分配结构值即可。如果你有一个接收 non-pointer Person
的函数,它已经是一个副本,所以只需 return 它的地址:
func copyPerson(p Person) *Person {
return &p
}
就是这样,这将复制 Person
的所有当前和未来字段。
现在可能存在字段是指针或 header-like 值(如切片)的情况,它们应该是来自原始字段的 "detached"(更准确地说是来自指向的 object) ,在这种情况下,您确实需要进行手动调整,例如
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
添加新字段,这将导致 compile-time 错误,因为未键控的复合结构文字必须列出所有字段。同样,如果有人更改了可将新字段分配给旧字段的字段(例如,有人将具有相同类型的 2 个彼此相邻的字段交换),这将无济于事,也不鼓励未加密的文字。
包所有者最好在 Person
类型定义旁边提供一个复制构造函数。因此,如果有人更改 Person
,他/她应该负责保持 CopyPerson()
仍然可操作。正如其他人提到的,你应该已经有了单元测试,如果 CopyPerson()
不符合它的名字,它应该会失败。
最佳可行方案?
如果您不能将 CopyPerson()
放在 Person
类型旁边并让其作者维护它,请继续进行指针和 [=72= 的结构值复制和手动处理] 字段。
并且您可以创建 person2
类型,它是 Person
类型的 "snapshot"。如果原始 Person
类型更改,请使用空白全局变量接收 compile-time 警报,在这种情况下 copyPerson()
包含的源文件将拒绝编译,因此您将知道它需要调整.
这是如何完成的:
type person2 struct {
Name string
Age int
}
var _ = Person(person2{})
如果 Person
和 person2
的字段不匹配,则空白变量声明将不会编译。
上述 compile-time 检查的变体可能是使用类型化的-nil
指针:
var _ = (*Person)((*person2)(nil))