猿问

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

考虑这个例子。假设我有一个在我的代码库中无处不在的对象:


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

}

另一位开发人员出现并向结构添加了一个新Gender字段Person。但是,因为该copyPerson函数位于一段遥远的代码中,所以他们忘记更新copyPerson. 如果您在创建结构时省略参数,golang 不会抛出任何警告或错误,代码将编译并看起来工作正常;唯一的区别是该copyPerson方法现在无法复制Gender结构,并且结果copyPerson将Gender替换为 nil 值(例如空字符串)。


防止这种情况发生的最佳方法是什么?有没有办法让 golang 在特定的结构初始化中强制执行不丢失参数?是否有可以检测此类潜在错误的 linter?


手掌心
浏览 130回答 5
5回答

慕田峪4524236

我解决这个问题的方法是只使用NewPerson(params)而不是导出这个人。这使得获取person实例的唯一方法是通过您的New方法。package person// Struct is not exportedtype person struct {    Name string    Age  int    Gender bool}// We are forced to call the constructor to get an instance of personfunc New(name string, age int, gender bool) person {    return person{name, age, gender}}这迫使每个人都从同一个地方获得一个实例。当您添加一个字段时,您可以将它添加到函数定义中,然后在它们构造新实例的任何地方都会出现编译时错误,因此您可以轻松找到它们并修复它们。

SMILET

首先,您的copyPerson()函数名不副实。它复制a 的一些字段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,因为未键控的复合结构文字必须列出所有字段。同样,如果有人更改了可将新字段分配给旧字段的字段(例如,有人交换了具有相同类型的彼此相邻的 2 个字段),这将无济于事,而且不鼓励使用未加密的文字。包所有者最好在Person类型定义旁边提供一个复制构造函数。因此,如果有人发生变化Person,他/她应该负责保持CopyPerson()运营。正如其他人提到的,你应该已经有了单元测试,如果CopyPerson()不符合它的名字,它应该会失败。最佳可行方案?如果您不能将CopyPerson()next 放在类型旁边Person并让其作者维护它,请继续进行结构值复制和手动处理指针和类似标头的字段。您可以创建一个person2类型,它是该Person类型的“快照”。如果原始类型发生变化,使用空白全局变量接收编译时警报Person,在这种情况下,copyPerson()包含 的源文件将拒绝编译,因此您将知道它需要调整。这是可以做到的:type person2 struct {    Name string    Age  int}var _ = Person(person2{})Person如果和的字段person2不匹配,则空白变量声明将不会编译。上述编译时检查的一种变体可能是使用类型化nil指针:var _ = (*Person)((*person2)(nil))

慕妹3242003

惯用的方法是根本不这样做,而是使零值有用。复制函数的例子并没有真正意义,因为它完全没有必要——你可以说:copy := new(Person) *copy = *origPerson不需要专门的功能,也不必保持字段列表是最新的。如果你想要一个新实例的构造函数NewPerson,就像理所当然的那样,只需编写一个并使用它。Linters 在某些方面非常有用,但没有什么能比得上广为人知的最佳实践和同行代码审查。

素胚勾勒不出你

我不知道强制执行该规则的语言规则。也就是说,我会重新考虑这里的设计。如果Personstruct 在您的代码库中如此重要,请集中创建和复制它,以便“遥远的地方”不只是创建和移动Persons。重构您的代码,以便仅使用一个构造函数来构建Persons(可能类似于person.New返回 a person.Person),然后您将能够集中控制其字段的初始化方式。

一只斗牛犬

我能想出的最好的解决方案(而且不是很好)是定义一个与tempPerson该结构相同的新结构Person,并将其放在任何初始化新 Person 结构的代码附近,并更改初始化一个Person以便它改为将其初始化为 atempPerson但随后将其转换为 a 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并希望获得这种安全性的任何地方复制和粘贴结构定义。有没有更好的办法?
随时随地看视频慕课网APP

相关分类

Go
我要回答