Définir des méthodes dans Go

introduction

Functions vous permet d'organiser la logique en procédures répétables qui peuvent utiliser différents arguments à chaque exécution. Lors de la définition des fonctions, vous constaterez souvent que plusieurs fonctions peuvent opérer à chaque fois sur le même élément de données. Go reconnaît ce modèle et vous permet de définir des fonctions spéciales, appeléesmethods, dont le but est d'opérer sur des instances d'un type spécifique, appeléesreceiver. L'ajout de méthodes à des types vous permet de communiquer non seulement la nature des données, mais également la manière dont elles doivent être utilisées.

Définir une méthode

La syntaxe permettant de définir une méthode est similaire à celle permettant de définir une fonction. La seule différence est l'ajout d'un paramètre supplémentaire après le mot-cléfunc pour spécifier le récepteur de la méthode. Le destinataire est une déclaration du type sur lequel vous souhaitez définir la méthode. L'exemple suivant définit une méthode sur un type de structure:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    Creature.Greet(sammy)
}

Si vous exécutez ce code, le résultat sera:

OutputSammy says Hello!

Nous avons créé une structure appeléeCreature avec des champsstring pour unName et unGreeting. CeCreature a une seule méthode définie,Greet. Dans la déclaration du récepteur, nous avons assigné l'instance deCreature à la variablec afin que nous puissions nous référer aux champs desCreature lorsque nous assemblons le message d'accueil dansfmt.Printf .

Dans d’autres langues, le destinataire des invocations de méthodes est généralement désigné par un mot clé (par exemple, this ouself). Go considère le récepteur comme une variable comme une autre, vous pouvez donc le nommer comme vous le souhaitez. Le style préféré par la communauté pour ce paramètre est une version minuscule du premier caractère du type de récepteur. Dans cet exemple, nous avons utiliséc car le type de récepteur étaitCreature.

Dans le corps demain, nous avons créé une instance deCreature et spécifié des valeurs pour ses champsName etGreeting. Nous avons appelé la méthodeGreet ici en joignant le nom du type et le nom de la méthode avec un. et en fournissant l'instance deCreature comme premier argument.

Go fournit un autre moyen plus pratique d'appeler des méthodes sur les instances d'une structure, comme illustré dans cet exemple:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet()
}

Si vous exécutez ceci, le résultat sera le même que dans l'exemple précédent:

OutputSammy says Hello!

Cet exemple est identique au précédent, mais cette fois nous avons utilisédot notation pour invoquer la méthodeGreet en utilisant leCreature stocké dans la variablesammy comme récepteur. Il s'agit d'une notation abrégée pour l'appel de fonction dans le premier exemple. La bibliothèque standard et la communauté Go préfèrent tellement ce style que vous verrez rarement le style d'invocation de fonction affiché précédemment.

L'exemple suivant montre une des raisons pour lesquelles la notation par points est plus répandue:

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() Creature {
    fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
    return c
}

func (c Creature) SayGoodbye(name string) {
    fmt.Println("Farewell", name, "!")
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet().SayGoodbye("gophers")

    Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
}

Si vous exécutez ce code, la sortie ressemble à ceci:

OutputSammy says Hello!!
Farewell gophers !
Sammy says Hello!!
Farewell gophers !

Nous avons modifié les exemples précédents pour introduire une autre méthode appeléeSayGoodbye et également changéGreet pour renvoyer unCreature afin de pouvoir invoquer d'autres méthodes sur cette instance. Dans le corps demain, nous appelons les méthodesGreet etSayGoodbye sur la variablesammy en utilisant d'abord la notation par points, puis en utilisant le style d'invocation fonctionnel.

Les deux styles produisent les mêmes résultats, mais l'exemple utilisant la notation par points est beaucoup plus lisible. La chaîne de points nous indique également la séquence dans laquelle les méthodes seront appelées, où le style fonctionnel inverse cette séquence. L'ajout d'un paramètre à l'appelSayGoodbye obscurcit encore plus l'ordre des appels de méthode. La clarté de la notation par points est la raison pour laquelle il s'agit du style préféré pour invoquer des méthodes dans Go, aussi bien dans la bibliothèque standard que parmi les packages tiers que vous trouverez dans l'écosystème Go.

La définition de méthodes sur des types, par opposition à la définition de fonctions qui agissent sur une valeur, a une autre signification particulière pour le langage de programmation Go. Les méthodes sont le concept de base derrière les interfaces.

Des interfaces

Lorsque vous définissez une méthode sur n’importe quel type dans Go, cette méthode est ajoutée auxmethod set du type. L'ensemble de méthodes est l'ensemble des fonctions associées à ce type en tant que méthodes et utilisées par le compilateur Go pour déterminer si un type peut être affecté à une variable avec un type d'interface. Uninterface type est une spécification de méthodes utilisées par le compilateur pour garantir qu'un type fournit des implémentations pour ces méthodes. Tout type qui a des méthodes avec le même nom, les mêmes paramètres et les mêmes valeurs de retour que ceux trouvés dans la définition d'une interface est dit àimplement cette interface et est autorisé à être affecté à des variables avec le type de cette interface. Voici la définition de l'interfacefmt.Stringer de la bibliothèque standard:

type Stringer interface {
  String() string
}

Pour qu'un type implémente l'interfacefmt.Stringer, il doit fournir une méthodeString() qui renvoie unstring. L'implémentation de cette interface permettra à votre type d'être imprimé exactement comme vous le souhaitez (parfois appelé «joli imprimé») lorsque vous passez des instances de votre type à des fonctions définies dans le packagefmt. L'exemple suivant définit un type qui implémente cette interface:

package main

import (
    "fmt"
    "strings"
)

type Ocean struct {
    Creatures []string
}

func (o Ocean) String() string {
    return strings.Join(o.Creatures, ", ")
}

func log(header string, s fmt.Stringer) {
    fmt.Println(header, ":", s)
}

func main() {
    o := Ocean{
        Creatures: []string{
            "sea urchin",
            "lobster",
            "shark",
        },
    }
    log("ocean contains", o)
}

Lorsque vous exécutez le code, vous verrez cette sortie:

Outputocean contains : sea urchin, lobster, shark

Cet exemple définit un nouveau type de structure appeléOcean. Ocean est dit àimplement l'interfacefmt.Stringer carOcean définit une méthode appeléeString, qui ne prend aucun paramètre et renvoie unstring. Dansmain, nous avons défini un nouveauOcean et l'avons passé à une fonctionlog, qui prend unstring pour imprimer en premier, suivi de tout ce qui implémentefmt.Stringer. Le compilateur Go nous permet de passero ici carOcean implémente toutes les méthodes demandées parfmt.Stringer. Danslog, nous utilisonsfmt.Println, qui appelle la méthodeString deOcean quand il rencontre unfmt.Stringer comme l'un de ses paramètres.

SiOcean ne fournissait pas de méthodeString(), Go produirait une erreur de compilation, car la méthodelog demande unfmt.Stringer comme argument. L'erreur ressemble à ceci:

Outputsrc/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
        Ocean does not implement fmt.Stringer (missing String method)

Go s'assurera également que la méthodeString() fournie correspond exactement à celle demandée par l'interfacefmt.Stringer. Si ce n'est pas le cas, cela produira une erreur ressemblant à ceci:

Outputsrc/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
        Ocean does not implement fmt.Stringer (wrong type for String method)
                have String()
                want String() string

Jusqu'à présent, nous avons défini des méthodes sur le récepteur de valeur. Autrement dit, si nous utilisons l'appel fonctionnel des méthodes, le premier paramètre, faisant référence au type sur lequel la méthode a été définie, sera une valeur de ce type, plutôt qu'unpointer. Par conséquent, toutes les modifications que nous apportons à l'instance fournie à la méthode seront ignorées à la fin de l'exécution de la méthode, car la valeur reçue est une copie des données. Il est également possible de définir des méthodes sur le type de destinataire du pointeur.

Récepteurs de pointeur

La syntaxe de définition des méthodes sur le récepteur de pointeur est presque identique à celle de méthodes de définition sur le récepteur de valeurs. La différence consiste à faire précéder le nom du type dans la déclaration du récepteur d'un astérisque (*). L'exemple suivant définit une méthode sur le récepteur de pointeur sur un type:

package main

import "fmt"

type Boat struct {
    Name string

    occupants []string
}

func (b *Boat) AddOccupant(name string) *Boat {
    b.occupants = append(b.occupants, name)
    return b
}

func (b Boat) Manifest() {
    fmt.Println("The", b.Name, "has the following occupants:")
    for _, n := range b.occupants {
        fmt.Println("\t", n)
    }
}

func main() {
    b := &Boat{
        Name: "S.S. DigitalOcean",
    }

    b.AddOccupant("Sammy the Shark")
    b.AddOccupant("Larry the Lobster")

    b.Manifest()
}

Vous verrez la sortie suivante lorsque vous exécutez cet exemple:

OutputThe S.S. DigitalOcean has the following occupants:
     Sammy the Shark
     Larry the Lobster

Cet exemple a défini un typeBoat avec unName etoccupants. Nous voulons forcer le code dans d'autres packages à ajouter uniquement des occupants avec la méthodeAddOccupant, nous avons donc rendu le champoccupants non exporté en minuscules la première lettre du nom du champ. Nous voulons également nous assurer que l'appel deAddOccupant entraînera la modification de l'instance deBoat, c'est pourquoi nous avons définiAddOccupant sur le récepteur du pointeur. Les pointeurs agissent comme une référence à une instance spécifique d'un type plutôt qu'une copie de ce type. Le fait de savoir queAddOccupant sera appelé à l'aide d'un pointeur versBoat garantit que toutes les modifications persisteront.

Dansmain, nous définissons une nouvelle variable,b, qui contiendra un pointeur vers unBoat (*Boat). Nous invoquons la méthodeAddOccupant deux fois sur cette instance pour ajouter deux passagers. La méthodeManifest est définie sur la valeurBoat, car dans sa définition, le récepteur est spécifié comme(b Boat). Dansmain, nous pouvons toujours appelerManifest car Go est capable de déréférencer automatiquement le pointeur pour obtenir la valeurBoat. b.Manifest() équivaut ici à(*b).Manifest().

Qu'une méthode soit définie sur un récepteur de pointeur ou sur un récepteur de valeurs a des implications importantes lorsque vous essayez d'affecter des valeurs à des variables qui sont des types d'interface.

Récepteurs et interfaces de pointeur

Lorsque vous affectez une valeur à une variable avec un type d'interface, le compilateur Go examine le jeu de méthodes du type en cours d'affectation pour s'assurer qu'il dispose des méthodes attendues par l'interface. Les ensembles de méthodes pour le récepteur de pointeur et le récepteur de valeur sont différents, car les méthodes qui reçoivent un pointeur peuvent modifier leur récepteur lorsque celles qui reçoivent une valeur ne le peuvent pas.

L’exemple suivant montre comment définir deux méthodes: l’une sur le récepteur du pointeur d’un type et sur son récepteur de valeurs. Cependant, seul le pointeur récepteur pourra satisfaire l'interface également définie dans cet exemple:

package main

import "fmt"

type Submersible interface {
    Dive()
}

type Shark struct {
    Name string

    isUnderwater bool
}

func (s Shark) String() string {
    if s.isUnderwater {
        return fmt.Sprintf("%s is underwater", s.Name)
    }
    return fmt.Sprintf("%s is on the surface", s.Name)
}

func (s *Shark) Dive() {
    s.isUnderwater = true
}

func submerge(s Submersible) {
    s.Dive()
}

func main() {
    s := &Shark{
        Name: "Sammy",
    }

    fmt.Println(s)

    submerge(s)

    fmt.Println(s)
}

Lorsque vous exécutez le code, vous verrez cette sortie:

OutputSammy is on the surface
Sammy is underwater

Cet exemple a défini une interface appeléeSubmersible qui attend des types ayant une méthodeDive(). Nous avons ensuite défini un typeShark avec un champName et une méthodeisUnderwater pour garder une trace de l'état desShark. Nous avons défini une méthodeDive() sur le récepteur du pointeur versShark qui a modifiéisUnderwater entrue. Nous avons également défini la méthodeString() du récepteur de valeur afin qu'il puisse imprimer proprement l'état desShark en utilisantfmt.Println en utilisant l'interfacefmt.Stringer acceptée parfmt.Println que nous avons examinés plus tôt. Nous avons également utilisé une fonctionsubmerge qui prend un paramètreSubmersible.

L'utilisation de l'interfaceSubmersible plutôt que d'un*Shark permet à la fonctionsubmerge de dépendre uniquement du comportement fourni par un type. Cela rend la fonctionsubmerge plus réutilisable car vous n'auriez pas à écrire de nouvelles fonctionssubmerge pour unSubmarine, unWhale ou tout autre futur habitant aquatique que nous n'aurions pas ' t pensé encore. Tant qu'ils définissent une méthodeDive(), ils peuvent être utilisés avec la fonctionsubmerge.

Dansmain, nous avons défini une variables qui est un pointeur vers unShark et avons immédiatement imprimés avecfmt.Println. Ceci montre la première partie de la sortie,Sammy is on the surface. Nous avons passés àsubmerge, puis appelé à nouveaufmt.Println avecs comme argument pour voir la deuxième partie de la sortie imprimée,Sammy is underwater.

Si nous avons changés pour être unShark plutôt qu'un*Shark, le compilateur Go produirait l'erreur:

Outputcannot use s (type Shark) as type Submersible in argument to submerge:
    Shark does not implement Submersible (Dive method has pointer receiver)

Le compilateur Go nous dit utilement queShark a une méthodeDive, elle vient juste d'être définie sur le récepteur du pointeur. Lorsque vous voyez ce message dans votre propre code, le correctif consiste à passer un pointeur vers le type d'interface en utilisant l'opérateur& avant la variable à laquelle le type de valeur est affecté.

Conclusion

Déclarer des méthodes dans Go n’est finalement pas différent de la définition de fonctions recevant différents types de variables. Les mêmes règles deworking with pointers s'appliquent. Go fournit certaines commodités pour cette définition de fonction extrêmement courante et les regroupe dans des ensembles de méthodes pouvant être raisonnés par des types d'interface. L'utilisation efficace de méthodes vous permettra de travailler avec des interfaces dans votre code afin d'améliorer la testabilité et de laisser une meilleure organisation pour les futurs lecteurs de votre code.

Si vous souhaitez en savoir plus sur le langage de programmation Go en général, consultez nosHow To Code in Go series.