Programmation orientée objet ?
Contrairement à Java ou à C++, Go ne se dit pas être un langage « Orienté Objet ». Il permet cependant un style de programmation moderne inspirée des concepts de la programmation OOP. Certains disent que Go est un langage post-OOP.
Encapsulation
Encapsulation (Programmation) - Wikipedia
En programmation, l’encapsulation désigne le regroupement de données avec un ensemble de routines qui en permettent la lecture et la manipulation. Ce principe est souvent accompagné du masquage de ces données brutes1 afin de s’assurer que l’utilisateur ne contourne pas l’interface qui lui est destinée.
Go nous offre les struct et les méthodes liées à ces structures pour implémenter l’encapsulation. On peut aussi
masquer les détails avec des attributs ou des méthodes privées (en choisissant un identifiant qui commence
par une minuscule). On peut donc dire que Go supporte l’encapsulation des données.
Héritage
L’héritage est une caractéristique clé de la programmation OOP, mais Go ne supporte pas l’héritage comme Java ou C++. En revanche, Go remplace l’héritage par la composition.
On illustre habituellement l’héritage dans les langages OOP avec la modélisation des formes géométriques (un rectangle est un polygone, un polygone est une figure géométrique, un cercle est une figure géométrique…), des êtres vivants (un humain est un mammifère, un mammifère est un être vivant …) ou les véhicules (un camion est un véhicule, une voiture est un véhicule…). Considérons ce dernier et regardons comment structurer l’information en Go.
Le schéma de nos données est le suivant :

Le type de base Vehicle est modélisé avec une structure simple :
type Vehicle struct {
Weight float32
}
et on lui associe une méthode Start :
func (v *Vehicle) Start() {
fmt.Println("Starting Vehicle")
}
Pour le type Car nous disons qu’une voiture est composée d’un véhicule et nous
donnons le nom Base à ce type de base :
type Car struct {
Base Vehicle
Model string
}
On peut redéfinir la méthode Start pour ce nouveau type Car :
func (c *Car) Start() {
fmt.Println("Starting " + c.Model)
}
Pour le vélo, nous utilisons aussi la composition, mais cette fois, nous la faisons de manière anonyme, c’est-à-dire que nous ne donnons pas de nom au type de base :
type Bicycle struct {
Vehicle
WheelSize float32
}
et nous définissons la méthode Fold qui permet de plier le vélo (oui, c’est bien un vélo pliable)
func (b *Bicycle) Fold() {
fmt.Println("Folding Bicycle")
}
Nous pouvons maintenant créer des instances de ces nouveaux types. Une voiture c de type
Car peut être créée ainsi :
c := &Car{
Base: Vehicle{
Weight: 2600,
},
Model: "Bentley",
}
et un vélo b de type Bicycle alors :
b := &Bicycle{
Vehicle: Vehicle{
Weight: 20,
},
WheelSize: 28,
}
On peut appeler la méthode Start de la voiture c :
c.Start()
et nous obtenons :
Starting Bentley
Si nous appelons cette même méthode sur le vélo b :
b.Start()
nous obtenons :
Starting Vehicle
Comme la méthode Start n’est pas définie pour le type Bicycle, go appelle la
méthode attachée au type de base (Vehicle)
Comme le type de base de la voiture (Car) a un nom (Base), on peut aussi appeler
la méthode Start de ce type de base :
c.Base.Start()
nous obtenons également :
Starting Vehicle
Supposons maintenant que nous avons les classes suivantes :

- Un animal peut manger.
- Un chien est un animal et il aboie
- Un chat est un animal et il miaule
- Un robot peut être enclenché
- Un robot nettoyeur est un robot et il nettoie
- Un robot marcheur est un robot et il marche
Supposons maintenant que nous souhaitons une classe pour représenter Spot, un robot marcheur qui ressemble à un chien et qui peut aboyer

Il est très difficile de modéliser Spot en fonction de ce qu’il est. Par contre, c’est facile de la modéliser compte tenu de ce qu’il fait. Nous décomposons les types par rapport aux actions (par exemple un objet qui peut aboyer est un aboyeur, un objet qui peut marcher est un marcheur) et nous construisons les types initiaux par composition :

Un chien est désormais composé d’un mangeur et d’un aboyeur et nous pouvons modéliser Spot comme étant composé d’un aboyeur, d’un enclenchable et d’un marcheur.
Penser un programme en termes de composition au lieu d’héritage demande un petit temps d’adaptation et très souvent, le résultat est au moins tout aussi élégant.
Constructeurs
Il n’y a pas de constructeur explicite en Go, mais il est très courant de mettre
à disposition une fonction qui construit un objet. Cette fonction est
typiquement nommée New<Type>. Par exemple, pour créer un Robot, on écrit
typiquement une fonction NewRobot qui retourne un pointeur vers un Robot :
type Robot struct {
Name string
}
func NewRobot(name string) *Robot {
return &Robot{
Name: name,
}
}
Polymorphisme
Le polymorphisme en Go est réalisé à l’aide d’interfaces. Si l’on reprend
l’exemple précédent avec les véhicules (Vehicle), les voitures (Car)
et les vélos (Bicycle). Si l’on veut pouvoir démarrer une collection de véhicules,
on définira l’interface suivante :
type Starter interface {
Start()
}
Si b est un vélo (Bicycle) et c est une voiture (Car), on peut
définir une collection de véhicules ainsi :
collection := []Starter{b, c}
et on démarre tous ces véhicules de cette façon :
for _, v := range collection {
v.Start()
}
Enums
Les enums de base tels qu’ils existent en C sont relativement simples. Toutefois, les langages orientés objet tels que Java ou Rust en ont fait des classes avec beaucoup plus de fonctionnalités.
En Go, il n’y a pas d’enum explicite, mais le mot clé iota lié à un groupe de constantes permet de définir une séquence.
Dans l’exemple suivant, les constantes North, South, East et West auront respectivement les valeurs
0, 1, 2, et 3 :
type Direction int
const (
North Direction = iota
South
East
West
)
On peut aussi utiliser le iota avec des constructions plus complexes, comme dans l’exemple suivant :
const (
_ = iota // ignore first value by assigning to blank identifier
KiB = 1 << (10 * iota)
MiB
GiB
)
Notez que ces constantes sont des entiers et Go ne permet pas de restreindre les valeurs d’un entier
à être dans un ensemble donné. Dans l’exemple ci-dessous, la fonction f prend un paramètre de
type Direction, mais ce n’est pas interdit de passer MiB comme argument.
func f(d Direction) {
fmt.Println(d)
}
func main() {
f(GiB) // no error
}
Cette liberté peut être vue comme une faiblesse du langage, mais d’une autre côté, elle
offre des constructions intéressantes. Si vous avez besoin de représenter 13 GiB, vous pouvez
simplement écrire 13 * GiB. Cette technique est d’ailleurs utilisée dans le package time
de la bibliothèque standard :
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
Une durée de 3 minutes s’écrit simplement 3 * time.Minute.
Génériques
Les génériques sont apparus récemment (mars 2022) dans la version 1.18 de Go. Nous n’avons pas encore beaucoup d’exemples de cas d’utilisation de cette nouvelle fonctionnalité, mais nous ne doutons pas qu’elle ouvre une nouvelle dimension au langage.
Itérateurs
A partir de Go 1.23, et motivé par l’introduction des génériques, Go a introduit
le concept d’itérateurs. Les itérateurs sont des objets qui permettent de parcourir
une collection d’éléments. Les itérateurs sont utilisés dans les boucles for pour
parcourir les éléments d’une collection.
Nous pouvons illustrer cela avec un exemple simple qui itère sur les fameux nombre de Fibonacci :
func IterateFibonacci(yield func(int64) bool) {
f0, f1 := int64(0), int64(1)
for yield(f0) {
f0, f1 = f1, f0+f1
}
}
func main() {
count := 0
for v := range IterateFibonacci {
println(v)
count++
if count == 50 {
break
}
}
}
Si vous souhaitez en savoir plus sur les itérateurs, vous pouvez consulter l’article de Bitfield Consulting.