Предотвратить пропущенные поля в инициализации структуры

Рассмотрим этот пример. Допустим, у меня есть этот объект, который вездесущ в моей кодовой базе:

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 заменяется нулевым значением (например, пустой строкой).

Каков наилучший способ предотвратить это? Есть ли способ попросить Golang обеспечить отсутствие отсутствующих параметров в конкретной инициализации структуры? Есть ли линтер, который может обнаружить этот тип потенциальной ошибки?

7 ответов

То, как я обычно решаю это, это просто использовать 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}
}

Это заставляет всех получать экземпляр из одного и того же места. Когда вы добавляете поле, вы можете добавить его в определение функции, а затем вы получите ошибки времени компиляции везде, где оно используется.

Прежде всего, ваш 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, потому что неопубликованный составной структурный литерал должен перечислить все поля. Опять же, это не поможет вам, если кто-то поменяет поля, где новые поля могут быть назначены старым (например, кто-то поменяет 2 поля рядом друг с другом, имеющих одинаковый тип), также не одобряются литералы без ключа.

Лучше всего, чтобы владелец пакета предоставил конструктор копирования рядом с Person определение типа. Так что если кто-то изменится Personон / она должен нести ответственность CopyPerson() все еще в рабочем состоянии. И как уже упоминалось, у вас уже должны быть модульные тесты, которые должны провалиться CopyPerson() не соответствует своему имени.

Лучший жизнеспособный вариант?

Если вы не можете разместить CopyPerson() сразу после Person напечатайте и сделайте так, чтобы его автор поддерживал его, продолжайте копирование значения структуры и ручную обработку полей указателя и заголовка.

И вы можете создать person2 тип, который является "снимком" Person тип. Используйте пустую глобальную переменную для получения предупреждения во время компиляции, если оригинал Person изменения типа, и в этом случае copyPerson()содержащий исходный файл откажется компилировать, так что вы будете знать, что он нуждается в корректировке.

Вот как это можно сделать:

type person2 struct {
    Name string
    Age  int
}

var _ = Person(person2{})

Пустое объявление переменной не будет компилироваться, если поля Person а также person2 не совпадают.

Разновидностью вышеупомянутой проверки во время компиляции может быть использование typed-nil указатели:

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

Идиоматическим способом было бы вообще не делать этого, а вместо этого сделать нулевое значение полезным. Пример функции копирования на самом деле не имеет смысла, потому что он совершенно не нужен - вы можете просто сказать:

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

и не нуждаются в выделенной функции и не должны обновлять список полей. Если вы хотите конструктор для новых экземпляров, таких как NewPerson, просто напишите один и используйте его как само собой разумеющееся. Линтеры хороши для некоторых вещей, но ничто не сравнится с хорошо понятыми лучшими практиками и проверкой кода.

Я не знаю о языковом правиле, которое обеспечивает это.

Но вы можете написать специальные проверки для Go ветеринара, если хотите. Вот недавний пост, говорящий об этом.


Тем не менее, я бы пересмотреть дизайн здесь. Если Person struct так важна в вашей кодовой базе, централизует ее создание и копирование, чтобы "отдаленные места" не просто создавались и перемещались Personвокруг. Рефакторинг вашего кода, чтобы для сборки использовался только один конструктор Persons (может быть что-то вроде person.New возвращая person.Person), и тогда вы сможете централизованно контролировать, как инициализируются его поля.

Подход 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: (не уверен, если это возможно)

Можно ли это покрыть тестами, скажем, с помощью отражения?
Если мы сравним количество инициализированных полей (инициализируем все поля значениями, отличными от значений по умолчанию) в исходной структуре и полями в копии, возвращенными функцией копирования.

Лучшее решение, которое я смог придумать (и оно не очень хорошее), это определить новую структуру 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 struct и хотел бы иметь эту безопасность. Есть ли лучший способ?

Вот как я бы это сделал:

func copyPerson(origPerson Person) *Person { 
    newPerson := origPerson

    //proof that 'newPerson' points to a new person object
    newPerson.name = "new name"
    return &newPerson
}

Go Playground

Другие вопросы по тегам