防止结构初始化丢失的字段

问题描述 投票:2回答:7

考虑下面这个例子。比方说,我有这样的对象,它是我的整个代码库无处不在:

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
}

另一位开发人员走来,并增加了一个新的领域GenderPerson结构。然而,由于copyPerson功能是在遥远的一段代码,他们忘记更新copyPerson。因为如果省略创建一个结构时,参数golang不会引发任何警告或错误,该代码将编译并出现做工精细;唯一的区别是,该方法copyPerson现在将故障转移Gender结构的复制,和copyPerson的结果将有Gender具有零值(例如空字符串)所取代。

什么是防止这种情况发生的最好方法是什么?有没有办法问golang执行特定结构的初始化没有缺少的参数?有没有可以检测到这种类型的潜在错误的棉绒?

go struct initialization composite-literals
7个回答
4
投票

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

这迫使每个人都来自同一个地方获得一个实例。当你添加一个字段,可以将其添加到函数定义,然后你随处编译它的使用时间的错误。


2
投票

惯用的方法是无法做到这一点在所有的,而是make the zero value useful。复印功能的例子并没有真正意义,因为它是完全没有必要的 - 你可以只说:

copy := new(Person)
*copy = *origPerson

而不需要专门的功能,也必须保持字段的列表保持最新状态。如果你想要像NewPerson新实例构造函数,只写一个,并把它作为理所当然的事。棉短绒是伟大的一些事情,但没有什么比充分理解的最佳实践和同行代码审查。


2
投票

首先,你copyPerson()功能不辜负它的名字。它拷贝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,因为unkeyed的复合结构字面必须列出所有字段。同样,如果有人更改其中新字段分配给旧的(例如有人掉期2场旁边彼此具有相同类型)的字段这不会帮助你,也无锁文字都望而却步。

最好将是包所有者提供一个拷贝构造函数,旁边Person类型定义。因此,如果有人改变Person,他/她应该负责保管CopyPerson()仍在运行。正如其他人所说,你应该已经有单元测试,如果CopyPerson()不辜负它的名字应该失败。

最可行的选择?

如果你不能把旁边CopyPerson()类型Person和具有它的作者维护它,继续与指针的结构值复制和人工搬运和头般的领域。

你可以创建一个person2型这是一种在Person类型的“快照”。使用空白全局变量接受编译时警告,如果原来的Person类型的变化,其中包括源文件的情况下copyPerson()的将拒绝编译,所以你就会知道它需要调整。

这是如何做到:

type person2 struct {
    Name string
    Age  int
}

var _ = Person(person2{})

如果Personperson2的领域不匹配空白变量声明不会编译。

上述编译时检查的变化可能是使用类型化nil指针:

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

1
投票

我不知道,强制执行语言规则。

但是你可以编写自定义跳棋为Go vet,如果你愿意的话。 Here's a recent post talking about that


这就是说,我会在这里重新设计。如果Person结构是在你的代码库非常重要的,它集中创建和复制,使“遥远的地方”不只是创建和移动Persons。重构代码,以便只有一个构造函数是用来建立Persons(可能像person.New返回person.Person),然后你就可以集中控制其字段如何初始化。


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结构,并希望有这样的安全,我不喜欢这种解决方案。有没有更好的办法?


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


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:(不知道这是可能的)

这可以在测试中被覆盖说使用反射? 如果我们比较初始化的字段数(初始化所有的字段值比默认值不同),在原来的结构和复制功能回到在副本中的字段。

© www.soinside.com 2019 - 2024. All rights reserved.