Utiliser des méthodes dans Go

Effectué

Une méthode dans Go est un type spécial de fonction avec une simple différence : vous devez inclure un paramètre supplémentaire avant le nom de la fonction. Ce paramètre supplémentaire est connu sous le nom de récepteur.

Les méthodes sont utiles quand vous voulez regrouper des fonctions et les lier à un type personnalisé. Cette approche dans Go est similaire à la création d’une classe dans d’autres langages de programmation, car elle vous permet d’implémenter certaines fonctionnalités du modèle POO, comme l’incorporation, la surcharge et l’encapsulation.

Pour comprendre pourquoi les méthodes sont importantes dans Go, commençons par voir comment vous en déclarez une.

Déclarer des méthodes

Jusqu’à présent, vous avez utilisé des structs seulement comme un autre type personnalisé que vous pouvez créer dans Go. Dans ce module, vous allez découvrir que, en ajoutant des méthodes, vous pouvez ajouter des comportements aux structs que vous créez.

La syntaxe pour déclarer une méthode se présente comme ceci :

func (variable type) MethodName(parameters ...) {
    // method functionality
}

Cependant, avant de pouvoir déclarer une méthode, vous devez créer un struct. Supposons que vous voulez créer un package de géométrie et que, dans le cadre de ce package, vous décidez de créer un struct de triangle appelé triangle. Vous voulez ensuite utiliser une méthode pour calculer le périmètre de ce triangle. Vous pouvez la représenter en Go comme suit :

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

Le struct ressemble à un struct normal, mais la fonction perimeter() a un paramètre supplémentaire de type triangle devant le nom de la fonction. Avec ce récepteur, lorsque vous utilisez le struct, vous pouvez appeler la fonction de la manière suivante :

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

Si vous essayez d’appeler la fonction perimeter() comme vous le feriez habituellement, cela ne va pas fonctionner, car la signature de la fonction indique qu’elle a besoin d’un récepteur. La seule manière d’appeler cette méthode est de déclarer d’abord un struct, qui vous donne accès à la méthode. Vous pouvez même avoir le même nom pour une méthode pour autant qu’elle appartient à un autre struct. Par exemple, vous pouvez déclarer un struct square avec une fonction perimeter() comme suit :

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

Si vous exécutez le code précédent, vous remarquez qu’il n’y a pas d’erreur et vous recevez la sortie suivante :

Perimeter (triangle): 9
Perimeter (square): 16

Dans les deux appels à la fonction perimeter() , le compilateur détermine la fonction à appeler en fonction du type du récepteur. Ce comportement contribue à garantir la cohérence et des noms courts dans les fonctions des packages, et évite d’avoir à inclure le nom du package en préfixe. Nous expliquerons pourquoi ce comportement est potentiellement important quand nous aborderons les interfaces dans l’unité suivante.

Pointeurs dans les méthodes

Dans certains cas, une méthode doit mettre à jour une variable. Ou, si l’argument passé à la méthode est trop grand, vous voulez éviter de le copier. Dans ces situations, vous devez utiliser des pointeurs pour passer l’adresse d’une variable. Dans un module précédent, quand nous avons présenté les pointeurs, nous avons dit que chaque fois que vous appelez une fonction en Go, Go fait une copie de chaque valeur d’argument pour l’utiliser.

Le même comportement se produit quand vous devez mettre à jour la variable du récepteur dans une méthode. Par exemple, supposons que vous souhaitiez créer une méthode pour doubler la taille du triangle. Vous devez utiliser un pointeur dans la variable du récepteur, comme suit :

func (t *triangle) doubleSize() {
    t.size *= 2
}

Vous pouvez prouver que la méthode fonctionne, comme ceci :

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

Quand vous exécutez le code précédent, vous obtenez normalement la sortie suivante :

Size: 6
Perimeter: 18

Vous n’avez pas besoin d’un pointeur dans la variable du récepteur quand la méthode accède simplement aux informations du récepteur. Cependant, la convention Go stipule que si une méthode d’un struct a un récepteur de pointeur, toutes les méthodes de ce struct doivent aussi avoir un récepteur de pointeur, même si une de ces méthodes n’en a pas besoin.

Déclarer des méthodes pour d’autres types

Un aspect essentiel des méthodes est de les définir pour n’importe quel type, et pas seulement pour des types personnalisés comme les structs. Cependant, vous ne pouvez pas définir un struct à partir d’un type qui appartient à un autre package. Par conséquent, vous ne pouvez pas créer une méthode sur un type de base, comme string.

Néanmoins, vous pouvez utiliser une ruse pour créer un type personnalisé à partir d’un type de base, puis vous l’utilisez comme s’il s’agissait du type de base. Par exemple, supposons que vous voulez créer une méthode pour transformer une chaîne de lettres minuscules en majuscules. Vous pouvez écrire un code similaire à celui-ci :

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

Learning Go!
LEARNING GO!

Notez que vous pouvez utiliser le nouvel objet s comme s’il s’agissait d’une chaîne quand vous affichez sa valeur. Ensuite, quand vous appelez la méthode Upper, s affiche toutes les lettres majuscules de type string.

Incorporer des méthodes

Dans un module précédent, vous avez découvert que vous pouvez utiliser une propriété dans un struct et incorporer la même propriété dans un autre struct. Autrement dit, vous pouvez réutiliser les propriétés d’un struct pour éviter la répétition et maintenir la cohérence dans votre base de code. Une idée similaire s’applique aux méthodes. Vous pouvez appeler des méthodes du struct incorporé même si le récepteur est différent.

Par exemple, supposons que vous voulez créer un nouveau struct de triangle avec une logique pour inclure une couleur. De plus, vous voulez continuer à utiliser le struct de triangle que vous avez déclaré avant. Le struct de triangle coloré se présenterait donc comme ceci :

type coloredTriangle struct {
    triangle
    color string
}

Vous pouvez ensuite initialiser le struct coloredTriangle et appeler la méthode perimeter() du struct triangle (et même accéder à ses champs) comme suit :

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Continuez et incluez les modifications précédentes dans votre programme pour voir comment l’incorporation fonctionne. Quand vous exécutez le programme avec une méthode main() comme la précédente, vous devez obtenir la sortie suivante :

Size: 3
Perimeter 9

Si vous connaissez bien un langage de POO comme Java ou C++, vous pouvez penser que le struct triangle ressemble à une classe de base et que coloredTriangle est une sous-classe (comme l’héritage), mais ce n’est pas correct. Ce qui se passe en réalité, c’est que le compilateur Go promeut la méthode perimeter() en créant une méthode wrapper, qui se présente comme ceci :

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

Notez que le récepteur est coloredTriangle, qui appelle la méthode perimeter() à partir du champ du triangle. La bonne nouvelle est que vous n’avez pas besoin de créer la méthode précédente. Vous pourriez, mais Go le fait pour vous en coulisse. Nous avons inclus l’exemple précédent seulement à des fins d’apprentissage.

Surcharger des méthodes

Revenons à l’exemple de triangle que nous avons présenté précédemment. Que se passe-t-il si vous voulez modifier l’implémentation de la méthode perimeter() dans le struct coloredTriangle ? Vous ne pouvez pas avoir deux fonctions avec le même nom. Cependant, comme les méthodes nécessitent un paramètre supplémentaire (le récepteur), vous êtes autorisé à avoir une méthode portant le même nom, pour autant qu’elle soit spécifique au récepteur que vous voulez utiliser. Cette distinction s’opère au niveau de la façon dont vous surchargez les méthodes.

En d’autres termes, vous pouvez écrire la méthode wrapper que nous avons vue si vous voulez modifier son comportement. Si le périmètre d’un triangle coloré est le double du périmètre d’un triangle normal, le code doit se présenter comme ceci :

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

Maintenant, sans rien changer d’autre dans la méthode main() que vous avez écrite précédemment, il se présente comme ceci :

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Quand vous l’exécutez, vous obtenez une sortie différente :

Size: 3
Perimeter 18

Cependant, si vous avez encore besoin d’appeler la méthode perimeter() du struct triangle, vous pouvez le faire en y accédant explicitement, comme ceci :

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

Quand vous exécutez ce code, vous obtenez normalement la sortie suivante :

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

Comme vous l’avez peut-être remarqué, en Go, vous pouvez remplacer une méthode et néanmoins accéder à la méthode d’origine si vous en avez besoin.

Encapsulation dans les méthodes

Encapsulation signifie qu’une méthode est inaccessible à l’appelant (client) d’un objet. Habituellement, dans d’autres langages de programmation, vous placez les mots clés private ou public devant le nom de la méthode. En Go, vous devez utiliser seulement un identificateur avec une majuscule pour rendre une méthode publique et un identificateur sans majuscule pour rendre une méthode privée.

L’encapsulation dans Go prend effet seulement entre des packages. Autrement dit, vous pouvez seulement masquer les détails de l’implémentation d’un autre package, mais pas le package lui-même.

Pour faire un essai, créez un nouveau package geometry et placez-y le struct de triangle, comme suit :

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

Vous pourriez utiliser le package précédent, comme ceci :

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

Et la sortie serait la suivante :

Perimeter 18

Si vous essayez d’appeler le champ size ou la méthode doubleSize() à partir de la fonction main(), le programme va « paniquer », comme ceci :

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

Quand vous exécutez le code précédent, vous obtenez l’erreur suivante :

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)