Aller au contenu

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
}