Blog des Gens Compliqués

Golang, c'est bien

20/07/2024 10:45:31+02:00|Par DkVZ
8 minutes de lecture (facile)

En 2020 (lol) je parlais de remplacer Perl et Java (LOL) par notamment JavaScript et C#.

C'est pas un secret que j'aime beaucoup Rust pour son efficacité de l'extrême et la robustesse de ses outils (Cargo, intégration des tests, ...).

Par contre, il faut bien avouer que coder en Rust c'est lent. Je passe plein de temps à optimiser les chaines d'appels sur des Option et Result et à créer des tests dans le module en cours pour tester ce que je suis en train d'écrire (ce qui est une bonne chose en fait) et tout ça me prend beaucoup de temps.

Si j'ai un soucis avec le borrow checker ou les durées de vie, ça va aussi me retarder même si avec l'expérience on intègre les moyens d'éviter ce type de soucis.

Je suis toujours partagé entre l'idée d'obtenir l'efficacité maximale au niveau mémoire et juste m'autoriser quelques copies de contenu de variables par-ci par-là ou l'utilisation de types de pointeurs complexes.

L'absence de valeurs "null", j'aime beaucoup aussi, mais comme j'en parlais plus haut ça force à avoir un bon nombre de chaines de fonctions sur les Option (c'est l'élément qui remplace les valeurs "null") ou des match à gogo.

Du coup c'est bien mais ça se paye en temps de dev.

Go n'est pas vraiment comparable à Rust en cela qu'il embarque un garbage collector qui ajoute de la complexité de fonctionnement aux exécutables compilés, qui utilisent aussi générallement plus de mémoire.

Ceci dit, la plupart du temps et pour la plupart des cas d'utilisation, qui est à 30 MB de mémoire près? Personne, non?

Le garbage collecteur de Go est aussi très efficace et se comporte très différemment de celui de Java qui est légèrement effrayant pour les vieux devs/admin sys comme moi-même.

Tout est plus simple tout en étant horriblment efficace.

Ce qui m'amène finalement à juste utiliser Go plutôt que C#. Même si la nature des langages est totalement différente puisque Go n'est pas basé sur des classes.

C'est d'autant moins grave que quand on regarde les évolutions de C#, on dirait qu'il s'éloignent volontairement de plus en plus des vieux paradigmes orientés objet en décourageant l'héritage (plutôt utiliser des méthodes d'extension et interfaces) et en ajoutant énormément d'aspects empruntés à la programmation fonctionnelle.

A ce sujet, les fonctions sont des éléments de première classe en Go, c'est à dire qu'on peut les assigner à des variables ou créer des fonctions qui renvoient de nouvelles fonctions. Comme en JavaScript, par exemple.

Double gain: j'ai un langage très efficace en terme de multitâche (j'en discute plus loin) avec les fondations des paradigmes fonctionnels que j'apprécie beaucoup en JavaScript (qui est "mauvais" en multitâche — Utilise une approche totalement différente pour ça).

Il a aussi une notion de valeurs par défaut qui fonctionne très bien, c'est à dire que si j'oublie d'initialiser un entier, ben il vaut 0. Comme en C#. Par contre pour obtenir cette fonctionnalité ils ont dû ajouter la notion de "null" (qui s'appelle "nil") mais franchement c'est pas trop un problème quand on voit le paradigme de gestion d'erreur.

Et du coup j'ai plus besoin de C#.

J'ai redessiné le logo de Go, c'est pas super joli
C'est le logo de Go, les auteurs l'ont fait avec paint et l'utilisent toujours à l'identique

Je pensais que Go était davantage un langage système mais pas du tout, il est très simple à prendre en main et même s'il a une notion de pointeurs, elle est bien plus simple que celle de C/C++ (ou Rust).

La plupart du temps on a même pas besoin des pointeurs, certains objets comme un flux Writer par exemple, peuvent se passer à des fonctions par valeur parce qu'ils contiennent eux-mêmes les pointeurs nécessaires.

Gestion d'erreur

C'est le truc qui me faisait un peu peur en Go. La gestion d'erreur est basée sur renvoi de valeurs multiples par les fonctions, en général un tuple avec les données utiles et un truc-qui-implémente-l'interface-Error.

func getMessage() (string, error) {
  // Logique programmative intense
  // ...

  // Happy path:
  return "Coucou", nil
}

func main() {
  msg, _ := getMessage()
  fmt.Printf("Getting mah message: %s", msg)
}

Ce qui est intéressant c'est que les programmes peuvent vaguement se poursuivre même sans gestion d'erreur parce que les données utiles (ici msg par ex.) sont tout de même initialisées à leur valeur par défaut (une chaîne vide dans ce cas précis).

Evidemment on pourrait tomber plus loin sur une erreur fatale, mais il y a un réel potentiel de CODAGE A L'ARRACHE offert par ce paradigme. Et c'est très clair qu'on est en pur ARACHE parce qu'on est obligé d'utiliser le placeholder "_" pour nier l'erreur, parce qu'en Go, si une variable est déclarée mais pas utilisée, il refuse totalement de compiler.

Ce qui est un peu dingo. En Rust j'ai parfois des variables pas utilisées, ce qui génère tout un tas d'avertissements mais ça compile quand même.

Cependant quand on comprend le paradigme de gestion d'erreur, ça prend tout son sens. Même si ça a l'air moche vu de loin.

Une des critiques principales de Go est que le dit paradigme mènent à tout un tas de lignes qui ressemblent à ça:

if err != nil {
  return
}

Avec évidemment la possiblité d'avoir quelques éléments avant le "return".

Tout est conçu en Go pour permettre d'utiliser le plan "si y a un soucis, je return avant la fin de la fonction". Ils ont même ajouté un keyword defer pour que ça puisse toujours fonctionner de manière sûre. C'EST UN TRUC DE DINGUE je vous dis.

Du coup ça a l'air bizarre mais ça force doucement (Rust quand il te force c'est PAS doucement) à:

  • Ecrire les fonctions de la même manière logique, rend les projets Go plus lisibles;
  • Gérer toutes les erreurs. Si on veut travailler proprement, y a pas le choix;
  • Penser à ce qu'il se passe si l'utilisateur ne gère pas les erreurs mais avec un filet (il se retrouve avec une valeur par défaut).

Tout cela ne m'empêche pas d'apprécier la gestion d'erreur en Rust qui est aussi excellente mais elle est formellement obligatoire et prend aussi plus de temps parce qu'il faut chipoter quel type de Result on va utiliser et comment on va le gérer.

Les Goroutines

Un peu dans la même veine que la gestion d'erreur, Go force un paradigme spécifique pour le multitâche.

Il est ultra simple à aborder tout nu puisqu'il s'agit en l'essence de l'ajout d'un vieux mot clé "go".

Quand ce mot clé est devant une exécution de fonction (qui peut-être une fonction en ligne), cette fonction est exécutée en parallèle pendant que le processeur passe à la suite du programme.

Et voilà. C'est tout.

C'est ultra simple de créer un parallel for avec ce paradigme.

package main

import (
	"fmt"
	"time"
)

func main() {
	y := 0
	for i := 0; i < 10; i++ {
		go func() {
			y++
		}()
	}

	time.Sleep(5 * time.Second)

	fmt.Printf("y vaut maintenant %v", y)
}

Bon il est tout pourri cet exemple. Déjà y a la notion de closure en cela que ma fonction "en-ligne" s'approprie la variable "y" sans discussions (d'aucuns sachent que c'est plus complexe en Rust).

Mais surtout, si on omet le time.Sleep, le programme dit que "y" vaut 0 (la plupart du temps — Il arrive que ça soit un autre chiffre < 10). Parce que le programme continue après tous les appels qui suivent le mot clé "go", il arrive à la fin et n'attend pas que les goroutines s'achèvent.

Par contre, si j'attends 5 seconds exprès (on peut attendre beaucoup moins de ça évidemment, c'est pour l'illustration), la variable "y" vaut 10.

C'est-y pas trop cool?

Il semblerait que ce paradigme évite les deadlocks là où l'async-await de C# n'en serait pas capable. Enfin c'est ce que j'ai lu sur les internets, je n'ai jamais réussi à deadlock quoi que ce soit en C# et j'ai mon password manager qui tourne sans interruption depuis des années.

Utiliser juste "go" comme ça n'est évidemment pas suffisant pour un scénario même un petit peu avancé parce qu'il n'y a aucune protection contre les race conditions.

Go fournit des channels qui sont très simples à utiliser et aussi intégrés au langage que les Goroutines. Je vous laisse regarder si ça vous intéresse.

A côté de ça il y a les habituels verrous.

Attends, ça compile

Je sais pas comment ils font mais le compilateur Go est extrêmement rapide et n'a pas l'air de "laisser sa trace" comme LLVM et Rust qui me télécharge 5GB de dépendances et divers symboles et libraires pour une compilation locale.

Cargo qui compile un projet Rust, énormément de lignes 'compiling'
Vasy bouffe tes 3000 dépendances en version 0.truc lol trop bien

Le projet peut être organisé un peu comme on veut, il faut juste demande de "go build" un fichier ".go" qui a une fonction main (quand il s'agit de construire un exécutable).

Tous les fichiers ".go" d'un même répertoire fonctionnent comme s'ils étaient un seul gros fichier, tout est aplati dans le même espace de nom comme si on était des gros barbares ou des programmeurs PHP.

Pour indiquer que quelque chose doit être public en dehors du package, il suffit de commencer son nom par une majuscule. Oui c'est tout.

C'est tellement simple que j'ai envie de pleurer.

Etant donné la flexibilité du truc, c'est courant d'avoir des Makefile ou autre Taskfile dans les projets Go mais pour automatiser des tâches bien plus simples que ce qu'on peut voir dans un projet C/C++.

Difficultés?

Pour l'instant j'ai encore du mal avec les assignations par "=" ou ":=", je mélange parfois un peu tout.

Vous aurez peut-être remarqué qu'à l'instar de mon JavaScript, y a pas de points-virgules dans le code Go.

Ben en fait si. Et c'est exactement comme en JavaScript, le compilateur insère automatiquement les points-virgules.

Du coup il y a parfois des gags quand on essaye de découper certaines déclarations sur plusieurs lignes, il faut parfois ajouter des virgules surprise. Qui n'ont rien à faire là. Oui c'est bizarre.

Par contre je préfère ça qu'avoir des points-virgules partout.

Conclusion

C'est vraiment un chouette langage! Je pense qu'avec une bonne maitrise pratique on peut vraiment rapidement prototyper et développer avec assez peu de compromis à accepter en terme de performances et d'utilisation mémoire. Je crois que c'est ça tout le thème du language: un excellent équilibre de compromis de vitesse de développement, de complexité et de performance.

De plus les exécutables Go sont générallement très portables parce qu'il n'y a pas du tout de bibliothèques dynamiques qui sont utilisées — C'est théoriquement possible de compiler de cette manière en Rust mais sur certains projets dans mon cas ça ne fonctionne pas du tout (dû à des bindings C qui trainent?).

Cette compilation portable est possible pour plusieurs plateformes et, comme je le disais plus haut, est inexplicablement rapide.

Pour mes cas d'utilisations, ça ne va pas remplacer Rust, et générallement pas JavaScript non plus, quoi que j'y pense très sérieusement pour créer des API rapides.

Commentaires

Il faut JavaScript activé pour écrire des commentaires ici

Ajouter un commentaire

Votre commentaire a été ajouté
(enfin, je pense)