Méthodes en Go
La différence entre une méthode et une fonction en Go est que la méthode possède un récepteur, tandis que la fonction n'en a pas. Seuls les types personnalisés peuvent avoir des méthodes. Commençons par un exemple.
type IntSlice []int
func (i IntSlice) Get(index int) int {
return i[index]
}
func (i IntSlice) Set(index, val int) {
i[index] = val
}
func (i IntSlice) Len() int {
return len(i)
}Nous avons d'abord déclaré un type IntSlice, dont le type sous-jacent est []int, puis déclaré trois méthodes Get, Set et Len. L'apparence d'une méthode n'est pas très différente de celle d'une fonction, elle a juste un petit segment (i IntSlice) en plus. i est le récepteur, IntSlice est le type du récepteur. Le récepteur est similaire à this ou self dans d'autres langages, sauf qu'en Go, il doit être explicitement indiqué.
func main() {
var intSlice IntSlice
intSlice = []int{1, 2, 3, 4, 5}
fmt.Println(intSlice.Get(0))
intSlice.Set(0, 2)
fmt.Println(intSlice)
fmt.Println(intSlice.Len())
}L'utilisation d'une méthode est similaire à l'appel d'une méthode membre d'une classe : d'abord déclarer, puis initialiser, puis appeler.
Récepteur de valeur
Il existe deux types de récepteurs : le récepteur de valeur et le récepteur de pointeur. Commençons par un exemple :
type MyInt int
func (i MyInt) Set(val int) {
i = MyInt(val) // Modifié, mais cela n'a aucun effet
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}Après l'exécution du code ci-dessus, on constate que la valeur de myInt reste 1 et n'a pas été modifiée en 2. Lorsqu'une méthode est appelée, la valeur du récepteur est passée à la méthode. Dans l'exemple ci-dessus, le récepteur est un récepteur de valeur, qui peut être simplement considéré comme un paramètre formel. Modifier la valeur d'un paramètre formel n'a aucun effet sur la valeur en dehors de la méthode. Que se passe-t-il si on appelle via un pointeur ?
func main() {
myInt := MyInt(1)
(&myInt).Set(2)
fmt.Println(myInt)
}Malheureusement, un tel code ne peut toujours pas modifier la valeur interne. Pour pouvoir correspondre au type du récepteur, Go le déréférencera, interprété comme (*(&myInt)).Set(2).
Récepteur de pointeur
Avec une légère modification, on peut modifier correctement la valeur de myInt.
type MyInt int
func (i *MyInt) Set(val int) {
*i = MyInt(val)
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}Le récepteur est maintenant un récepteur de pointeur. Bien que myInt soit un type de valeur, lors de l'appel d'une méthode de récepteur de pointeur via un type de valeur, Go l'interprétera comme (&myint).Set(2). Ainsi, lorsque le récepteur de la méthode est un pointeur, la valeur interne peut être modifiée que l'appelant soit un pointeur ou non.
Lors du passage de paramètres d'une fonction, il y a une copie de valeur. Si un entier est passé, cet entier est copié. Si une tranche est passée, cette tranche est copiée. Mais si un pointeur est passé, seul le pointeur est copié. Il est évident que passer un pointeur consomme moins de ressources que passer une tranche. Il en va de même pour les récepteurs. Les récepteurs de valeur et les récepteurs de pointeur suivent la même logique. Dans la plupart des cas, il est recommandé d'utiliser un récepteur de pointeur. Cependant, les deux ne doivent pas être mélangés. Soit on utilise l'un, soit on utilise l'autre, mais pas les deux en même temps. Voici un exemple :
TIP
Il est nécessaire de comprendre d'abord les interfaces
type Animal interface {
Run()
}
type Dog struct {
}
func (d *Dog) Run() {
fmt.Println("Run")
}
func main() {
var an Animal
an = Dog{}
// an = &Dog{} Méthode correcte
an.Run()
}Ce code ne passera pas la compilation. Le compilateur affichera l'erreur suivante :
cannot use Dog{} (value of type Dog) as type Animal in assignment:
Dog does not implement Animal (Run method has pointer receiver)Traduit, cela signifie qu'il est impossible d'utiliser Dog{} pour initialiser une variable de type Animal, car Dog n'implémente pas Animal. Il existe deux solutions : premièrement, changer le récepteur de pointeur en récepteur de valeur ; deuxièmement, changer Dog{} en &Dog{}. Expliquons-les un par un.
type Dog struct {
}
func (d Dog) Run() { // Changé en récepteur de valeur
fmt.Println("Run")
}
func main() { // Peut fonctionner normalement
var an Animal
an = Dog{}
// an = &Dog{} Fonctionne également
an.Run()
}Dans le code original, le récepteur de la méthode Run est *Dog, donc naturellement c'est le pointeur Dog qui implémente l'interface Animal, et non la structure Dog. Ce sont deux types différents, donc le compilateur considère que Dog{} n'est pas une implémentation de Animal, et ne peut donc pas être assigné à la variable an. La deuxième solution consiste donc à assigner le pointeur Dog à la variable an. Cependant, lors de l'utilisation d'un récepteur de valeur, le pointeur Dog peut toujours être normalement assigné à animal, car Go déréférencera automatiquement le pointeur dans les cas appropriés, car on peut trouver la structure Dog via le pointeur. Dans le cas contraire, on ne peut pas trouver le pointeur Dog via la structure Dog. Si on mélange simplement les récepteurs de valeur et les récepteurs de pointeur dans la structure, cela n'a pas de conséquences graves, mais après utilisation avec des interfaces, des erreurs apparaîtront. Autant utiliser toujours des récepteurs de valeur ou toujours des récepteurs de pointeur, former une bonne norme, et réduire également la charge de maintenance ultérieure.
Il existe une autre situation : lorsque le récepteur de valeur est adressable, Go insérera automatiquement l'opérateur de pointeur pour l'appel. Par exemple, une tranche est adressable, et on peut toujours modifier sa valeur interne via un récepteur de valeur. Voici un code :
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func main() {
s := make(Slice, 1)
s.Set(0, 1)
fmt.Println(s)
}Sortie :
[1]Mais cela posera un autre problème : si on ajoute des éléments, la situation sera différente. Voici l'exemple :
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func (s Slice) Append(a int) {
s = append(s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}[1]La sortie est toujours la même. La fonction append a une valeur de retour. Après avoir ajouté des éléments à la tranche, il faut écraser la tranche d'origine, surtout après un redimensionnement. Modifier le récepteur de valeur dans la méthode n'aura aucun effet, ce qui conduit au résultat de l'exemple. En changeant en récepteur de pointeur, cela fonctionne normalement.
type Slice []int
func (s *Slice) Set(i int, v int) {
(*s)[i] = v
}
func (s *Slice) Append(a int) {
*s = append(*s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}Sortie :
[1 2]