5
votes

Empêcher les champs manquants dans l'initialisation de la structure

Prenons cet exemple. Disons que j'ai cet objet qui est omniprésent dans ma base de code:

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

Quelque part au fond de la base de code, j'ai aussi du code qui crée une nouvelle structure Person . Peut-être que c'est quelque chose comme la fonction utilitaire suivante (notez que ce n'est qu'un exemple d'une fonction qui crée une Person - le but de ma question n'est pas de poser spécifiquement sur la fonction de copie): p>

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

Un autre développeur arrive et ajoute un nouveau champ Gender à la structure Person . Cependant, comme la fonction copyPerson est dans un morceau de code distant, ils oublient de mettre à jour copyPerson . Puisque golang ne lève aucun avertissement ou erreur si vous omettez un paramètre lors de la création d'une structure, le code se compilera et semblera fonctionner correctement; la seule différence est que la méthode copyPerson ne réussira plus à copier la structure Gender et que le résultat de copyPerson aura Gender remplacé par une valeur nulle (par exemple la chaîne vide).

Quelle est la meilleure façon d'éviter que cela ne se produise? Existe-t-il un moyen de demander à golang d'appliquer aucun paramètre manquant dans une initialisation de structure spécifique? Existe-t-il un linter capable de détecter ce type d'erreur potentielle?


2 commentaires

Le meilleur moyen serait d'écrire un linter personnalisé.


Un problème hypothétique facilement découvert lors des tests unitaires donc rien à craindre.


7 Réponses :


0
votes

La meilleure solution que j'ai pu trouver (et ce n'est pas très bon) est de définir une nouvelle structure tempPerson identique à la structure Person et de la mettre à proximité de tout code qui initialise une nouvelle structure Person, et pour changer le code qui initialise une Person afin qu'il l'initialise à la place en tant que tempPerson mais le convertit ensuite en Personne . Comme ceci:

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
}

De cette façon, si un autre champ Gender est ajouté à Person mais pas à tempPerson , le le code échouera au moment de la compilation. Vraisemblablement, le développeur verrait alors l'erreur, modifierait tempPerson pour qu'il corresponde à sa modification en Person et, ce faisant, remarquerait le code à proximité qui utilise tempPerson et reconnaissent qu'ils devraient éditer ce code pour gérer également le champ Gender .

Je n'aime pas cette solution car elle implique de copier et coller la définition de structure partout où nous initialiser une structure Person et souhaiterait avoir cette sécurité. Y a-t-il un meilleur moyen?


1 commentaires

Il y a une bonne idée là-dedans, mais il n'est pas nécessaire de construire une valeur de tempPerson et de la convertir et de l'attribuer à une Person à chaque fois copyPerson () est appelé. Après tout, vous avez juste besoin de savoir si les types de structure (leurs champs) correspondent, ce qui peut être effectué au moment de la compilation. Voir ma réponse sur la marche à suivre .



1
votes

Je ne connais pas de règle linguistique qui applique cela.

Mais vous pouvez écrire des vérificateurs personnalisés pour Aller chez le vétérinaire si vous le souhaitez. Voici un article récent qui en parle .


Cela dit, je reviendrais ici sur la conception. Si la structure Person est si importante dans votre base de code, centralisez sa création et sa copie afin que les «endroits distants» ne créent pas et ne déplacent pas simplement des Person s. Refactorisez votre code afin qu'un seul constructeur soit utilisé pour construire des Person s (peut-être quelque chose comme person.New renvoyant un person.Person ), et vous pourrez ensuite contrôler de manière centralisée la façon dont ses champs sont initialisés.


0 commentaires

0
votes

Voici comment procéder:

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

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

Go Playground


1 commentaires

Je ne pense pas qu'il demande une meilleure façon de copier une personne, il demande comment gérer le moment où des champs sont ajoutés à la structure, pour s'assurer que partout où une instance crée une instance définit la valeur et qu'elle n'obtienne pas seulement la valeur par défaut .



5
votes

La façon dont je résoudrais ceci est d'utiliser simplement NewPerson (params) et de ne pas exporter la personne. Ainsi, le seul moyen d'obtenir une instance de person est de passer par votre méthode New .

package person

// 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}
}

Cela oblige tout le monde à obtenir une instance du même endroit. Lorsque vous ajoutez un champ, vous pouvez l'ajouter à la définition de la fonction, puis vous obtenez des erreurs de compilation partout où ils construisent une nouvelle instance, afin que vous puissiez facilement les trouver et les corriger.


3 commentaires

Avoir des champs uniquement accessibles via les méthodes getter / setter est un Go unidiomatique.


@Adrian n'est pas vraiment le point de ce que je montre. L'utilisation d'interfaces est complètement idiomatique, et si vous avez besoin d'accéder aux champs, vous renvoyez simplement la structure au lieu de l'interface - les champs exportés d'une structure non exportée sont toujours accessibles.


En utilisant cette approche, il deviendra verbeux d'écrire des fonctions qui acceptent la structure person comme argument, les consommateurs dans l'autre package devront définir chaque champ eux-mêmes comme interface.



0
votes

Approche 1 Ajoutez quelque chose comme le constructeur de copie:

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

Approche 2: (je ne sais pas si cela est possible) p >

Cela peut-il être couvert dans des tests, disons en utilisant la réflexion?
Si l'on compare le nombre de champs initialisés (initialiser tous les champs avec des valeurs différentes des valeurs par défaut) dans la structure d'origine et les champs en copie retournés par la fonction de copie.


0 commentaires

1
votes

La manière idiomatique serait de ne pas faire cela du tout, et plutôt faire le zéro valeur utile . L'exemple d'une fonction de copie n'a pas vraiment de sens car il est totalement inutile - vous pouvez simplement dire:

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

et ne pas avoir besoin d'une fonction dédiée ni de garder une liste de champs à jour . Si vous voulez un constructeur pour de nouvelles instances telles que NewPerson , écrivez-en un et utilisez-le naturellement. Les linters sont parfaits pour certaines choses, mais rien ne vaut les meilleures pratiques bien comprises et la révision du code par les pairs.


0 commentaires

2
votes

Tout d'abord, votre fonction copyPerson () n'est pas à la hauteur de son nom. Il copie certains champs d'une Personne , mais pas (nécessairement) tous. Il aurait dû être nommé copySomeFieldsOfPerson () .

Pour copier une valeur struct complète, affectez simplement la valeur struct. Si vous avez une fonction recevant une Person non-pointeur, c'est déjà une copie, alors renvoyez simplement son adresse:

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

C'est tout, ceci copiera tous les champs présents et futurs de Person.

Maintenant, il peut y avoir des cas où les champs sont des pointeurs ou des valeurs de type en-tête (comme une tranche) qui devraient être "détachées" de le champ d'origine (plus précisément à partir de l'objet pointé), auquel cas vous devez faire des ajustements manuels, par exemple

type person2 struct {
    Name string
    Age  int
}

var _ = Person(person2{})

Ou une solution alternative qui ne fait pas une autre copie de p mais détache toujours Person.Data:

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

Bien sûr, si quelqu'un ajoute un champ qui nécessite également une manipulation manuelle , cela ne vous aidera pas.

Vous pouvez également utiliser un littéral sans clé, comme ceci:

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

Cela entraînera une erreur de compilation si quelqu'un ajoute un nouveau champ à Person , car un littéral de structure composite sans clé doit répertorier tous les champs. Encore une fois, cela ne vous aidera pas si quelqu'un modifie les champs où les nouveaux champs sont assignables aux anciens (par exemple, quelqu'un échange 2 champs l'un à côté de l'autre ayant le même type), les littéraux sans clé sont également déconseillés.

Le mieux serait que le propriétaire du package fournisse un constructeur de copie, à côté de la définition de type Person . Donc, si quelqu'un change de Personne , il / elle devrait être responsable de garder CopyPerson () toujours opérationnel. Et comme d'autres l'ont mentionné, vous devriez déjà avoir des tests unitaires qui devraient échouer si CopyPerson () ne respecte pas son nom.

La meilleure option viable?

Si vous ne pouvez pas placer le CopyPerson () à côté du type Person et que son auteur le maintienne, continuez avec la copie de la valeur de structure et la gestion manuelle du pointeur et des champs de type en-tête.

Et vous pouvez créer un type person2 qui est un "instantané" du type Person . Utilisez une variable globale vide pour recevoir une alerte à la compilation si le type d'origine de Person change, auquel cas le fichier source contenant copyPerson () refusera de compiler, vous vous saurez qu'elle doit être ajustée.

Voici comment procéder:

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
}

La déclaration de variable vide ne sera pas compilée si les champs de La personne et la personne2 ne correspondent pas.

Une variante de la vérification au moment de la compilation ci-dessus pourrait être d'utiliser des pointeurs typés nil :

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


0 commentaires