Предотвратить пропущенные поля в инициализации структуры
Рассмотрим этот пример. Допустим, у меня есть этот объект, который вездесущ в моей кодовой базе:
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 ©
}
Другой разработчик приходит и добавляет новое поле 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
вокруг. Рефакторинг вашего кода, чтобы для сборки использовался только один конструктор Person
s (может быть что-то вроде 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 ©
}
Таким образом, если другое поле 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
}