Search code examples
gostructinitializationcomposite-literals

Prevent missing fields in struct initialization


Consider this example. Let's say I have this object which is ubiquitous throughout my codebase:

type Person struct {
    Name string
    Age  int
    [some other fields]
}

Somewhere deep in the codebase, I also have some code that creates a new Person struct. Maybe it's something like the following utility function (note that this is just an example of some function that creates a Person-- the point of my question is not to ask about the copy function specifically):

func copyPerson(origPerson Person) *Person {
    copy := Person{
        Name: origPerson.Name,
        Age:  origPerson.Age,
        [some other fields]
    }
    return &copy
}

Another developer comes along and adds a new field Gender to the Person struct. However, because the copyPerson function is in a distant piece of code they forget to update copyPerson. Since golang doesn't throw any warning or error if you omit a parameter when creating a struct, the code will compile and appear to work fine; the only difference is that the copyPerson method will now fail to copy over the Gender struct, and the result of copyPerson will have Gender replaced with a nil value (e.g. the empty string).

What is the best way to prevent this from happening? Is there a way to ask golang to enforce no missing parameters in a specific struct initialization? Is there a linter that can detect this type of potential error?


Solution

  • First of all, your copyPerson() function does not live up to its name. It copies some fields of a Person, but not (necessarily) all. It should've been named copySomeFieldsOfPerson().

    To copy a complete struct value, just assign the struct value. If you have a function receiving a non-pointer Person, that is already a copy, so just return its address:

    func copyPerson(p Person) *Person {
        return &p
    }
    

    That's all, this will copy all present and future fields of Person.

    Now there may be cases where fields are pointers or header-like values (like a slice) which should be "detached" from the original field (more precisely from the pointed object), in which case you do need to make manual adjustments, e.g.

    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
    }
    

    Or an alternative solution which does not make another copy of p but still detaches Person.Data:

    func copyPerson(p Person) *Person {
        var data []byte
        p.Data = append(data, p.Data...)
        return &p
    }
    

    Of course, if someone adds a field which also needs manual handling, this won't help you out.

    You could also use unkeyed literal, like this:

    func copyPerson(p Person) *Person {
        return &Person{
            p.Name,
            p.Age,
        }
    }
    

    This will result in a compile-time error if someone adds a new field to Person, because an unkeyed composite struct literal must list all fields. Again, this will not help you out if someone changes the fields where the new fields are assignable to the old ones (e.g. someone swaps 2 fields next to each other having the same type), also unkeyed literals are discouraged.

    Best would be for the package owner to provide a copy constructor, next to the Person type definition. So if someone changes Person, he / she should be responsible keeping CopyPerson() still operational. And as others mentioned, you should already have unit tests which should fail if CopyPerson() does not live up to its name.

    The best viable option?

    If you can't place the CopyPerson() next to the Person type and have its author maintain it, go ahead with the struct value copying and manual handling of pointer and header-like fields.

    And you can create a person2 type which is a "snapshot" of the Person type. Use a blank global variable to receive compile-time alert if the original Person type changes, in which case copyPerson()'s containing source file will refuse to compile, so you'll know it needs adjusting.

    This is how it can be done:

    type person2 struct {
        Name string
        Age  int
    }
    
    var _ = Person(person2{})
    

    The blank variable declaration will not compile if fields of Person and person2 do not match.

    A variation of the above compile-time check could be to use typed-nil pointers:

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