Quand j'ai essayé Rust pour la première fois, il y a environ deux ans, j'y comprenais rien, et j'arrivais pas à comprendre la hype autour de ce langage. Je venais de langages plus haut-niveau, où la mémoire était gérée toute seule entre autres (JavaScript, Vala, ou même Python parfois). Donc quand on me disait "cannot move out of borrowed content", je comprenais pas trop ce qui se passait.
Mais j'ai persévéré, j'ai lu de la documentation, je me suis lancé dans de vrais projets avec Rust, et maintenant j'arrive non seulement à l'utiliser sans trop me poser de questions, mais j'arrive aussi à apprécier ces aspects que je détestais au début (rien de plus décourageant qu'un programme qui devrait compiler mais qui en fait ne compile pas, surtout quand on ne comprend rien à l'erreur).
Mais aujourd'hui, je ne suis pas là pour parler de Rust, mais de Go ! Je vais surtout utiliser Rust comme un point de comparaison parce que les deux langages se placent plus où moins au même niveau : ils veulent garantir du code performant (le code est compilé en langage machine dans les deux cas), et permettre de paralléliser à fond ses programmes sans se prendre la tête.
Tout comme pour Rust il y a quelques années, j'avais beaucoup de mal à comprendre la hype autour du Go, il y a peu de temps de ça. De ce que j'en voyais, la syntaxe me semblait bizarre et le langage lui-même vraiment mal pensé. On aurait dit du C avec une bonne bibliothèque standard, et deux nouveaux mot-clés pour faire de la programmation parallèle.
Mais, il y a quelques jours, j'ai eu l'idée d'un super projet (dont je vais bientôt parler, ne vous inquiétez pas 😏️). Le seul souci, c'est que j'avais des besoins un peu particuliers, à savoir : pouvoir faire du WebRTC côté serveur (WebRTC c'est la techno qui permet de faire du P2P dans les navigateurs, de base pour de la visioconférence, mais ça a aussi été détourné : c'est ce que Peertube utilise par exemple). Donc pour ça j'avais pas trop le choix, les seuls langages à avoir une bibliothèque pour faire du WebRTC étaient le Go ou le C++ (en compilant WebKit, au moins en partie). Et le JavaScript, vu que Peertube utilise ça apparemment.
J'avais pas vraiment envie de faire du JS, et du C++ n'en parlons pas (surtout si il fallait compiler WebKit 😅️), alors que je m'étais dit que ça serait bien d'essayer le Go pour de vrai une fois, voir si c'était si horrible que ce que je pensais ou si la hype était méritée. Du coup, voici le bilan !
Ce que j'aime pas
Déjà, la syntaxe est vraiment bizarre. Ça je pense que je m'y ferais jamais, même si je refais du Go. Rien n'est dans l'ordre auquel on est habitué, et ça donne l'impression que les créateurs du langage pensaient que faire un nouveau langage ça nécessitait une syntaxe originale, alors que pas du tout. Mais c'est une opinion très personnelle, et en vrai la syntaxe est généralement assez lisible.
Un des avantages de Go qui revient souvent serait qu'il y a une bonne bibliothèque standard. Alors, en effet, on peut faire un serveur HTTP très facilement juste avec ce qu'on trouve dans net/http
. Mais venant de Rust, il y a au moins une chose qui m'a manqué : les collections et la façon dont on peut les manipuler (ça fait un peu deux choses du coup). En Go, on a le choix entre des tableaux (arrays) ou des tables (maps), qui sont "inclus" dans le langage. Comme il n'y a pas de types génériques, c'est assez dur de créer ses propres collections, et on a donc pas du tout le choix de l'implémentation. Par exemple, dans certains cas une HashMap
(je reprends les noms en Rust) sera moins intéressante qu'une BTreeMap
, mais en Go on ne peut pas choisir. Ça veut aussi dire qu'il n'y a aucun type de Set
.
En plus de ça, comme je l'ai dit, la manipulation de ces collections est assez bizarre. En Rust, il y a le merveilleux trait Iterator
qui permet de faire plein de choses de manière standard et concise, avec n'importe quelle collection qui l'implémente. Par exemple, si on a un Vec<u32>
(une liste d'entiers) et qu'on veut garder que ceux qui sont pairs, puis les mettre dans un nouveau Vec<u32>
, on peut utiliser Iterator::filter
vu que Vec<T>
implémente Iterator
quelque soit T
:
let nombre_pairs = tous_les_nombres.into_iter()
.filter(|x| x % 2 == 0)
.collect::<Vec<u32>>();
C'est vraiment un truc super pratique en Rust, qui sert dans n'importe quel programme de plus de 30 lignes, et il y a pas d'équivalent en Go. Il faut passer par une boucle for
et par le mot-clé range
:
nombres_pairs := []int{}
for _, x := range tous_les_nombres {
if x % 2 == 0 {
nombres_pairs = append(nombres_pairs, x)
}
}
Ce qui est plus complexe à lire à mon avis : on doit comprendre tout l'algorithme nous même, alors qu'en Rust, une fois qu'on a compris ce que filter
faisait, on comprend immédiatement à chaque fois qu'on le voit.
Autre truc que j'ai trouvé nul (c'est le cas de le dire) en Go : le système de types. Principalement parce que nil
existe, et existe beaucoup trop souvent, ce qui fait qu'on se retrouve à toujours devoir penser à vérifier si une valeur est nulle ou si elle est bien là. Mais je vais pas m'étendre là dessus, parce que ce qui m'a le plus manqué, c'est les énumérations (de Rust, celles qu'il y a en C ou autre c'est pas si bien que ça). Rien que Option
et Result
auraient été tellement utiles, je me serais sentie beaucoup plus à l'aise si j'avais pu utiliser ça. Mais c'est juste pas la philosophie de Go, à la place on a nil
, et des fonctions qui retournent (resultat, erreur)
… là aussi je vais pas m'étendre dessus, il y a déjà plein d'articles qui expliquent pourquoi c'est triste qu'un langage « moderne » ait fait ces choix.
Un autre truc qui me semble pas être une bonne idée, et dont j'avais jamais entendu parler, mais que j'ai découvert, c'est les « zero values ». L'idée c'est que chaque type a une valeur par défaut, et qu'à moins qu'on en spécifie une autre cette valeur qu'aura une variable ou un champ non-initialisé. Par exemple :
type struct Exemple {
Un int
Deux string
}
var a = Exemple{}
Ici a
a pour valeur Exemple{ Un: 0, Deux: "" }
, parce qu'on a pas précisé de valeurs pour ces champs et donc la valeur par défaut a été utilisée. Ça peut être très pratique dans certains cas, mais il suffit d'oublier une fois que c'est comme ça que Go fonctionne, et on peut passer des heures à chercher pourquoi telle variable a telle valeur alors qu'elle ne devrait pas (j'ai vécu ça, je recommande pas vraiment).
Et enfin, dernier truc qui m'a énormément agacé : les tags. En Go, on peut annoter les champs d'une structure avec des tags, des sortes de méta-informations sur le champ (en Rust, l'équivalent serait les attributs que les proc-macros peuvent proposer). Petit exemple (qui vient de mon projet) :
type User struct {
gorm.Model
Username string `gorm:"unique;not null" json:"userName"`
DisplayName string `json:"displayName"`
Email string `gorm:"unique;not null" json:"email"`
PasswordHash string `gorm:"not null" json:"-"`
Password string `gorm:"-" json:"password"`
Bio string `json:"bio"`
AvatarUrl string `json:"avatarUrl"`
Confirmed bool `json:"-"`
}
Les tags, c'est les trucs entre backticks à la fin de chaque lignes. Je trouve déjà que la syntaxe est illisible. Ensuite le compilateur ne vérifie pas ce qu'on met dedans, donc même si on oublie de fermer un guillemet par exemple, il ne dira rien (c'est juste un exemple hein, ça ne m'est pas du tout arrivé une dizaine de fois 🙃️). Et comme c'est le seul moyen de contrôler le comportement de certaines libs, on se trouve vite limité : je pense notamment à gorm
(pour manipuler une base de données) qui permet d'ajouter des champs pour la date de création, d'édition et de suppression, ainsi qu'une ID à chaque modèle, en utilisant gorm.Model
comme je l'ai fait dans l'exemple ci-dessus. Sauf que comme ces champs sont gérés par GORM, on ne peut pas les personaliser, et j'ai donc dans mon API REST des objets qui ont un champ avatarUrl
ou email
à côté d'un autre qui s'appelle CreatedAt
. C'est pas très grave, mais ça casse toute la cohérence que je voulais pour écrire un front-end dans un JavaScript standard (donc en camelCase, et pas en PascalCase).
Bref, il y a plein d'autres défauts, mais d'autres articles en parlent bien mieux que moi.
Ce qui est cool
Mais malgré ça, tester le Go sur un vrai projet me l'a fait aimé plus que je ne pensait. Malgré tout ces défauts, il a un énorme avantage : il est simple à comprendre et à utiliser. Je n'avais jamais fait de Go avant, j'en avait lu quelques lignes au plus, mais en une semaine j'ai compris beaucoup de choses, et la plupart du temps je n'ai pas eu à me battre avec le compilateur (les quelques fois où j'ai eu des erreurs, j'ai tout de suite ou presque compris ce qui n'allait pas). Go n'a pas du tout la même philosophie que Rust, ça serait même l'opposé : en Rust, le compilateur veut absolument tout vérifier, là où en Go il nous fait confiance, il part du principe qu'on sait ce qu'on fait. Ça rend le langage très accessible, parce qu'il n'y a pas des milliers de règles pour éviter qu'on fasse des bêtises.
C'est le seul point fort que j'ai trouvé à Go je crois, mais ça a été très important parce que sans ça je pense que j'aurais très vite abandonné. Et ça explique aussi pourquoi les gens aiment ce langage selon moi : on écrit un algorithme sans se casser la tête avec des détails trop techniques, mais on a quand même de bonnes performances et un programme qui tient en un fichier.
Après ce point fort est aussi un inconvénient : on a toujours un petit stress de pas savoir si ça va buguer ou pas, là où en Rust, une fois que ça compile, on sait que le code fera vraiment ce qu'il est censé faire, et les endroits où ça peut planter sont marqués assez explicitement (en général avec unwrap
ou expect
). À cause de ça, je pense que je ne réutiliserais pas de Go à moins d'y être obligée, surtout maintenant que je maîtrise à peu prêt le Rust et que c'est plus facile pour moi que le Go. Mais l'expérience a été moins mauvaise que ce à quoi je m'attendais.
Comments
August 23, 2019 08:31
cc @orskou@laverdu.re
March 7, 2020 23:29
Nice read. J'aime bien Rust.