Предотвратить пропущенные поля в инициализации структуры - PullRequest
0 голосов
/ 05 февраля 2019

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

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 голосов
/ 05 февраля 2019

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

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

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

0 голосов
/ 05 февраля 2019

Прежде всего, ваша функция 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 не совпадают.

Вариант вышеупомянутогопроверка во время компиляции может заключаться в использовании типизированных nil указателей:

var _ = (*Person)((*person2)(nil))
0 голосов
/ 05 февраля 2019

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

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

0 голосов
/ 05 февраля 2019

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

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

0 голосов
/ 05 февраля 2019

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

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 голосов
/ 05 февраля 2019

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

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


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

0 голосов
/ 05 февраля 2019

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

...