Je connais le fait que, dans Go, les interfaces définissent la fonctionnalité plutôt que les données. Vous placez un ensemble de méthodes dans une interface, mais vous ne pouvez pas spécifier de champs qui seraient requis sur tout ce qui implémente cette interface.
Par exemple:
// Interface
type Giver interface {
Give() int64
}
// One implementation
type FiveGiver struct {}
func (fg *FiveGiver) Give() int64 {
return 5
}
// Another implementation
type VarGiver struct {
number int64
}
func (vg *VarGiver) Give() int64 {
return vg.number
}
Nous pouvons maintenant utiliser l'interface et ses implémentations:
// A function that uses the interface
func GetSomething(aGiver Giver) {
fmt.Println("The Giver gives: ", aGiver.Give())
}
// Bring it all together
func main() {
fg := &FiveGiver{}
vg := &VarGiver{3}
GetSomething(fg)
GetSomething(vg)
}
/*
Resulting output:
5
3
*/
Maintenant, ce que vous ne pouvez pas faire est quelque chose comme ceci:
type Person interface {
Name string
Age int64
}
type Bob struct implements Person { // Not Go syntax!
...
}
func PrintName(aPerson Person) {
fmt.Println("Person's name is: ", aPerson.Name)
}
func main() {
b := &Bob{"Bob", 23}
PrintName(b)
}
Cependant, après avoir joué avec les interfaces et les structures embarquées, j'ai découvert un moyen de le faire, d'une certaine manière:
type PersonProvider interface {
GetPerson() *Person
}
type Person struct {
Name string
Age int64
}
func (p *Person) GetPerson() *Person {
return p
}
type Bob struct {
FavoriteNumber int64
Person
}
En raison de la structure intégrée, Bob a tout ce que Person a. Il implémente également l'interface PersonProvider, afin que nous puissions transmettre à Bob des fonctions conçues pour utiliser cette interface.
func DoBirthday(pp PersonProvider) {
pers := pp.GetPerson()
pers.Age += 1
}
func SayHi(pp PersonProvider) {
fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}
func main() {
b := &Bob{
5,
Person{"Bob", 23},
}
DoBirthday(b)
SayHi(b)
fmt.Printf("You're %v years old now!", b.Age)
}
Voici un Go Playground qui illustre le code ci-dessus.
En utilisant cette méthode, je peux créer une interface qui définit les données plutôt que le comportement, et qui peut être implémentée par n'importe quelle structure simplement en incorporant ces données. Vous pouvez définir des fonctions qui interagissent explicitement avec ces données intégrées et qui ne connaissent pas la nature de la structure externe. Et tout est vérifié au moment de la compilation! (La seule façon dont vous pourriez gâcher, que je peux voir, serait d'incorporer l'interface PersonProvider
dans Bob
, plutôt que dans un béton Person
. Cela compilerait et échouerait à l'exécution.)
Maintenant, voici ma question: est-ce un bon truc, ou devrais-je le faire différemment?
Réponses:
C'est définitivement une belle astuce. Cependant, exposer des pointeurs permet toujours d'accéder directement aux données, de sorte qu'il ne vous procure qu'une flexibilité supplémentaire limitée pour les modifications futures. De plus, les conventions Go ne vous obligent pas à toujours placer une abstraction devant vos attributs de données .
En prenant ces choses ensemble, je tendrais vers un extrême ou l'autre pour un cas d'utilisation donné: soit a) il suffit de créer un attribut public (en utilisant l'incorporation le cas échéant) et de passer des types concrets, soit b) s'il semble que l'exposition des données causer des problèmes plus tard, exposez un getter / setter pour une abstraction plus robuste.
Vous allez peser cela sur une base par attribut. Par exemple, si certaines données sont spécifiques à l'implémentation ou si vous prévoyez de changer les représentations pour une autre raison, vous ne souhaitez probablement pas exposer l'attribut directement, alors que d'autres attributs de données peuvent être suffisamment stables pour que les rendre publics soit une nette victoire.
Le masquage des propriétés derrière les getters et les setters vous donne une flexibilité supplémentaire pour apporter des modifications rétrocompatibles ultérieurement. Disons que vous voulez un jour changer
Person
pour stocker non seulement un seul champ "nom", mais le premier / milieu / dernier / préfixe; si vous avez des méthodesName() string
etSetName(string)
, vous pouvezPerson
satisfaire les utilisateurs existants de l' interface tout en ajoutant de nouvelles méthodes plus fines. Ou vous voudrez peut-être être en mesure de marquer un objet sauvegardé par une base de données comme "sale" lorsqu'il a des modifications non enregistrées; vous pouvez le faire lorsque les mises à jour des données passent toutes par desSetFoo()
méthodes.Donc: avec les getters / setters, vous pouvez modifier les champs de structure tout en conservant une API compatible, et ajouter une logique autour de la propriété get / sets car personne ne peut se passer de
p.Name = "bob"
passer par votre code.Cette flexibilité est plus pertinente lorsque le type est compliqué (et que la base de code est grande). Si vous avez un
PersonCollection
, il peut être sauvegardé en interne par unsql.Rows
, un[]*Person
, un[]uint
des ID de base de données, ou autre. En utilisant la bonne interface, vous pouvez éviter aux appelants de se soucier de ce que c'est, de la façon dontio.Reader
les connexions réseau et les fichiers se ressemblent.Une chose spécifique:
interface
s dans Go ont la propriété particulière que vous pouvez en implémenter un sans importer le package qui le définit; cela peut vous aider à éviter les importations cycliques . Si votre interface renvoie un*Person
, au lieu de simplement des chaînes ou autre, tousPersonProviders
doivent importer le package oùPerson
est défini. Cela peut être bien ou même inévitable; c'est juste une conséquence à connaître.Mais encore une fois, la communauté Go n'a pas de convention forte contre l'exposition des membres de données dans l'API publique de votre type . Il est laissé à votre jugement s'il est raisonnable d'utiliser l'accès public à un attribut dans le cadre de votre API dans un cas donné, plutôt que de décourager toute exposition, car cela pourrait éventuellement compliquer ou empêcher un changement d'implémentation plus tard.
Ainsi, par exemple, le stdlib fait des choses comme vous permettre d'initialiser un
http.Server
avec votre configuration et promet qu'un zérobytes.Buffer
est utilisable. C'est bien de faire vos propres trucs comme ça, et, en fait, je ne pense pas que vous devriez faire abstraction des choses de manière préventive si la version plus concrète et exposant les données semble susceptible de fonctionner. Il s'agit simplement d'être conscient des compromis.la source
Si je comprends bien, vous souhaitez remplir un champ struct dans un autre. Mon avis de ne pas utiliser d'interfaces pour s'étendre. Vous pouvez facilement le faire par la prochaine approche.
https://play.golang.org/p/aBJ5fq3uXtt
Remarque
Person
dans laBob
déclaration. Cela rendra le champ struct inclus disponible dans laBob
structure directement avec du sucre syntaxique.la source