防止在结构初始化中丢失字段

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 &copy
}

另一位开发人员出现并向 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 &copy
}

这样,如果另一个字段 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。重构您的代码,以便仅使用一个构造函数来构建 Persons(可能类似于 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
}

Go Playground

我要解决这个问题的方法是只使用 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{})

如果 Personperson2 的字段不匹配,则空白变量声明将不会编译。

上述 compile-time 检查的变体可能是使用类型化的-nil 指针:

var _ = (*Person)((*person2)(nil))