防止结构初始化中缺少字段

时间:2019-02-05 18:15:20

标签: go struct initialization composite-literals

请考虑以下示例。假设我有一个在我的代码库中无处不在的对象:

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

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

防止这种情况发生的最佳方法是什么?有没有一种方法可以让golang在特定的结构初始化中不强制执行任何遗失的参数?有没有一种短毛绒可以检测到这种潜在的错误?

7 个答案:

答案 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{})

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

以上编译时检查的一种变体可能是使用类型化的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 &copy
}

这样,如果将另一个字段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
}

Go Playground

答案 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:(不确定是否可行)

在使用反射的测试中可以覆盖吗?
如果我们比较原始结构中初始化的字段数(将所有字段初始化为具有不同于默认值的值)和复制函数返回的副本中的字段。