Méthodes et Interfaces (Go Tour 2/4)
Classes et Méthodes
Go n’a pas de classes, mais nous pouvons associer des méthodes aux types. Une méthode, c’est juste une fonction avec un paramètre récepteur (receiver argument) :
type Point struct {
X, Y float64
}
func (p Point) DistanceFromOrigin() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
Dans l’exemple ci-dessus, p est le receiver et c’est l’équivalent de this en C++ ou en Java.
La différence, c’est que vous pouvez nommer ce receiver comme vous voulez.
En Go, les méthodes ne sont pas limitées à être attachées à des struct. On peut par exemple
attacher une méthode à un simple nombre :
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
Notez au passage que Go n’a pas d’opération ternaire du genre condition ? true : false et
que la technique utilisée dans la fonction Abs est idiomatique en Go.
Notez aussi que la méthode doit être dans le même package que le type et nous
ne pouvons donc pas attacher de méthode à un type tel que float64. Mais
nous pouvons définir un alias comme ci-dessus avec MyFloat.
Récepteur de type pointeur
Le récepteur, tout comme les arguments d’une fonction ou d’une méthode, est passé par valeur; c’est-à-dire que la méthode en reçoit une copie. Si on souhaite modifier le récepteur, on doit le passer par référence en utilisant un pointeur :
func (p *Point) HorizontalMove(f float64) {
p.X += f
}
Pour appeler la méthode liée à un pointeur, vous n’avez pas besoin de prendre l’adresse de la variable :
func main() {
p := Point{3, 4}
p.HorizontalMove(10)
}
Attention
Go ne donne pas de message d’erreur si vous modifiez le récepteur ou tout autre argument passé per valeur. La modification se fera juste sur la copie et n’aura pas d’effet sur la variable utilisée par l’appelant.
L’utilisation d’un pointeur est nécessaire si on souhaite modifier la valeur de récepteur, mais c’est aussi intéressant d’utiliser un pointeur pour éviter de devoir copier le contenu du récepteur. On gagne ainsi en performance.
Dans les bonnes pratiques, on évite de mélanger des récepteurs par valeur et par références pour les méthodes d’un type donné.
Interfaces
Une interface en Go définit un ensemble de signatures de méthodes. Par exemple :
type geometry interface {
area() float64
perim() float64
}
L’interface ci-dessus est compatible avec tous les types qui implémentent
(au travers d’une méthode associée) les méthodes area et perim.
En Go, il est très fréquent de définir un type interface avec une seule
méthode. Par exemple le type Stringer de package fmt :
type Stringer interface {
String() string
}
La convention est d’ajouter er au nom de la méthode (par exemple
Reader, Writer, Formatter).
Pour implémenter une interface, il suffit… de l’implémenter. Par exemple :
type Fraction struct {
num int
denom int
}
func (f Fraction) String() string {
return fmt.Sprintf("%v / %v", f.num, f.denom)
}
func main() {
f := Fraction{3, 4}
fmt.Println(f)
}
Le type Fraction possède une méthode liée String avec la bonne signature
(elle ne prend aucun argument et retourne un string). Le type Fraction implémente
donc l’interface fmt.Stringer et peut être utilisé partout où un fmt.Stringer
est attendu. Contrairement à Java, vous n’avez pas besoin de répéter qu’un type
implémente une interface. S’il implémente toutes les méthodes de l’interface,
alors il implémente aussi l’interface.
Interface vide
Une interface qui ne spécifie aucune méthode est appelée interface vide (empty interface) :
interface{}
Comme chaque type implémente au moins zéro méthode, tous les types sont compatibles avec une interface vide.
La méthode fmt.Print peut être appelée avec n’importe quel type de
paramètre et utilise donc l’interface vide comme type d’arguments.
Notez que Go définit le type any comme alias pour une interface vide.
Note
Le type any est un alias pour une interface vide. Notez que
la fonction Println utilise des arguments de longueur
variable (variadic function) de type any
func Println(a ...any) (n int, err error)
La fonction Marshal du package encoding/json prend
également un argument de type any
func Marshal(v any) ([]byte, error)
Comment font ces fonctions pour savoir comment traiter ces paramètres ?
La réponse est : avec la réflexion. Cette technique dépasse le cadre de ce cours, mais lsi ça vous intéresse, vous trouverez une bonne introduction sur la page The Laws of Reflection
Assertions de type
Pour une instance i d’une interface, on peut obtenir la valeur concrète
(de type T) de l’instance avec une assertion de type (Type assertion) :
t := i.(T)
Par exemple :
f := Fraction{3, 4}
var i fmt.Stringer = f
g := i.(Fraction)
La variable f est une instance de Fraction, i est une instance d’une
interface, g est à nouveau une instance de Fraction.
Si la valeur concrète n’est pas de type T, alors vous obtiendrez une
erreur au run-time.
Pour prévenir ce genre d’erreur, vous pouvez tester si la valeur d’une interface est bien d’un type donné :
g, ok := i.(Fraction)
La variable ok sera true seulement si la valeur de i est bien de type Fraction
Type Switch
L’instruction switch permet aussi de faire une sélection basée sur un type. Supposons que
i soit de type any :
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
Notez l’utilisation du mot clé type à la première ligne.
La gestion des erreurs
Contrairement à Java ou Python, Go n’a pas de construction spéciale pour gérer les exceptions.
Il fait appel à la capacité des fonctions à retourner plusieurs valeurs et il définit le type standard
error. Ce type fait partie du langage de base et est équivalent à :
type error interface {
Error() string
}
L’exemple ci-dessous illustre l’utilisation de l’erreur :
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
Lorsque tout se passe bien, la fonction strconv.Atoi retourne l’entier
correspondant à l’argument et la deuxième valeur de retour est nil. En cas
de problème, la deuxième valeur de retour est une instance de error.
Readers
Le package io spécifie l’interface io.Reader, qui permet de lire un flux de données.
La bibliothèque standard de Go propose de nombreuses implémentations de cette interface, notamment pour les fichiers ou les connexions réseau.
L’interface io.Reader possède une seule méthode Read :
type Reader interface {
Read(p []byte) (n int, err error)
}
Read remplit le slice de bytes avec des données et renvoie le nombre d’octets lus
et une valeur d’erreur. Elle renvoie notamment l’erreur io.EOF lorsque le flux est terminé.
Images
Pour travailler avec des images (bitmap), le package image propose l’interface
suivante :
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}