4 Go Language Critiques
Publié: 2022-03-11Go (alias Golang) est l'une des langues qui intéressent le plus les gens. En avril 2018, il occupait la 19e place dans l'indice TIOBE. De plus en plus de personnes passent de PHP, Node.js et d'autres langages à Go et l'utilisent en production. De nombreux logiciels sympas (comme Kubernetes, Docker et Heroku CLI) sont écrits à l'aide de Go.
Alors, quelle est la clé du succès de Go ? Il y a beaucoup de choses à l'intérieur de la langue qui la rendent vraiment cool. Mais l'une des principales choses qui ont rendu Go si populaire est sa simplicité, comme l'a souligné l'un de ses créateurs, Rob Pike.
La simplicité est cool : vous n'avez pas besoin d'apprendre beaucoup de mots clés. Cela rend l'apprentissage des langues très facile et rapide. Cependant, d'un autre côté, les développeurs manquent parfois de certaines fonctionnalités qu'ils ont dans d'autres langages et, par conséquent, ils doivent coder des solutions de contournement ou écrire plus de code à long terme. Malheureusement, Go manque de nombreuses fonctionnalités de par sa conception, et parfois c'est vraiment ennuyeux.
Golang était censé accélérer le développement, mais dans de nombreuses situations, vous écrivez plus de code que vous n'en écririez avec d'autres langages de programmation. Je décrirai certains de ces cas dans mes critiques du langage Go ci-dessous.
Les critiques du langage 4 Go
1. Absence de surcharge de fonctions et de valeurs par défaut pour les arguments
Je vais poster un exemple de code réel ici. Lorsque je travaillais sur la liaison Selenium de Golang, j'avais besoin d'écrire une fonction à trois paramètres. Deux d'entre eux étaient facultatifs. Voici à quoi cela ressemble après la mise en œuvre :
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
J'ai dû implémenter trois fonctions différentes car je ne pouvais pas simplement surcharger la fonction ou transmettre les valeurs par défaut - Go ne le fournit pas par conception. Imaginez ce qui se passerait si j'appelais accidentellement la mauvaise personne ? Voici un exemple :
Je dois admettre que parfois la surcharge de fonctions peut entraîner un code désordonné. D'un autre côté, à cause de cela, les programmeurs doivent écrire plus de code.
Comment peut-il être amélioré ?
Voici le même (enfin, presque le même) exemple en JavaScript :
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
Comme vous pouvez le voir, cela semble beaucoup plus clair.
J'aime aussi l'approche Elixir à ce sujet. Voici à quoi cela ressemblerait dans Elixir (je sais que je pourrais utiliser des valeurs par défaut, comme dans l'exemple ci-dessus - je le montre simplement comme une manière de procéder):
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")
2. Manque de génériques
C'est sans doute la fonctionnalité que les utilisateurs de Go demandent le plus.
Imaginez que vous vouliez écrire une fonction de carte, où vous transmettez le tableau d'entiers et la fonction, qui sera appliquée à tous ses éléments. Cela semble facile, non ?
Faisons-le pour les entiers :
package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
Ça a l'air bien, non ?
Eh bien, imaginez que vous deviez également le faire pour les chaînes. Vous devrez écrire une autre implémentation, qui est exactement la même à l'exception de la signature. Cette fonction aura besoin d'un nom différent, car Golang ne prend pas en charge la surcharge de fonctions. En conséquence, vous aurez un tas de fonctions similaires avec des noms différents, et cela ressemblera à ceci :
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
Cela va définitivement à l'encontre du principe DRY (Don't Repeat Yourself), qui stipule que vous devez écrire le moins de code copier/coller possible et le déplacer vers des fonctions et les réutiliser.
Une autre approche consisterait à utiliser des implémentations uniques avec interface{}
comme paramètre, mais cela peut entraîner une erreur d'exécution car la vérification du type d'exécution est plus sujette aux erreurs. Et aussi ce sera plus lent, donc il n'y a pas de moyen simple d'implémenter ces fonctions comme une seule.
Comment peut-il être amélioré ?
Il existe de nombreux langages de qualité qui incluent le support des génériques. Par exemple, voici le même code dans Rust (j'ai utilisé vec
au lieu de array
pour le rendre plus simple):
fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }
Notez qu'il existe une seule implémentation de la fonction map
et qu'elle peut être utilisée pour tous les types dont vous avez besoin, même ceux personnalisés.
3. Gestion des dépendances
Quiconque a de l'expérience en Go peut dire que la gestion des dépendances est vraiment difficile. Les outils Go permettent aux utilisateurs d'installer différentes bibliothèques en exécutant go get <library repo>
. Le problème ici est la gestion des versions. Si le responsable de la bibliothèque apporte des modifications incompatibles avec les versions antérieures et les télécharge sur GitHub, toute personne essayant d'utiliser votre programme après cela obtiendra une erreur, car go get
ne fait rien d'autre que git clone
votre référentiel dans un dossier de bibliothèque. De plus, si la bibliothèque n'est pas installée, le programme ne se compilera pas à cause de cela.

Vous pouvez faire un peu mieux en utilisant Dep pour gérer les dépendances (https://github.com/golang/dep), mais le problème ici est que vous stockez toutes vos dépendances sur votre référentiel (ce qui n'est pas bon, car votre référentiel sera contenir non seulement votre code, mais des milliers et des milliers de lignes de code de dépendance), ou simplement stocker la liste des packages (mais encore une fois, si le responsable de la dépendance fait une modification incompatible avec les versions antérieures, tout plantera).
Comment peut-il être amélioré ?
Je pense que l'exemple parfait ici est Node.js (et JavaScript en général, je suppose) et NPM. NPM est un référentiel de packages. Il stocke les différentes versions des packages, donc si vous avez besoin d'une version spécifique d'un package, pas de problème, vous pouvez l'obtenir à partir de là. En outre, l'une des choses dans toute application Node.js/JavaScript est le fichier package.json
. Ici, toutes les dépendances et leurs versions sont répertoriées, vous pouvez donc toutes les installer (et obtenir les versions qui fonctionnent définitivement avec votre code) avec npm install
.
En outre, les bons exemples de gestion de packages sont RubyGems/Bundler (pour les packages Ruby) et Crates.io/Cargo (pour les bibliothèques Rust).
4. Traitement des erreurs
La gestion des erreurs dans Go est extrêmement simple. Dans Go, vous pouvez essentiellement renvoyer plusieurs valeurs à partir de fonctions, et la fonction peut renvoyer une erreur. Quelque chose comme ça:
err, value := someFunction(); if err != nil { // handle it somehow }
Imaginez maintenant que vous ayez besoin d'écrire une fonction qui effectue trois actions qui renvoient une erreur. Cela ressemblera à ceci :
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
Il y a beaucoup de code répétable ici, ce qui n'est pas bon. Et avec de grosses fonctions, ça peut être encore pire ! Vous aurez probablement besoin d'une touche sur votre clavier pour cela :
Comment peut-il être amélioré ?
J'aime l'approche de JavaScript à ce sujet. La fonction peut générer une erreur et vous pouvez l'attraper. Prenons l'exemple :
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
C'est beaucoup plus clair et il ne contient pas de code répétable pour la gestion des erreurs.
Les bonnes choses du Go
Bien que Go présente de nombreux défauts de conception, il possède également des fonctionnalités vraiment intéressantes.
1. Goroutines
La programmation asynchrone a été rendue très simple dans Go. Bien que la programmation multithreading soit généralement difficile dans d'autres langages, créer un nouveau thread et y exécuter une fonction afin qu'il ne bloque pas le thread actuel est très simple :
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Outils fournis avec Go
Alors que dans d'autres langages de programmation, vous devez installer différentes bibliothèques/outils pour différentes tâches (telles que les tests, le formatage de code statique, etc.), de nombreux outils sympas sont déjà inclus dans Go par défaut, tels que :
-
gofmt
- Un outil d'analyse de code statique. Par rapport à JavaScript, où vous devez installer une dépendance supplémentaire, commeeslint
oujshint
, ici, elle est incluse par défaut. Et le programme ne compilera même pas si vous n'écrivez pas de code de style Go (n'utilisez pas de variables déclarées, importez des packages inutilisés, etc.). -
go test
- Un cadre de test. Encore une fois, par rapport à JavaScript, vous devez installer des dépendances supplémentaires pour les tests (Jest, Mocha, AVA, etc.). Ici, il est inclus par défaut. Et cela vous permet de faire beaucoup de choses intéressantes par défaut, telles que l'analyse comparative, la conversion du code de la documentation en tests, etc. -
godoc
- Un outil de documentation. C'est bien de l'avoir inclus dans les outils par défaut. - Le compilateur lui-même. C'est incroyablement rapide, comparé à d'autres langages compilés !
3. Différer
Je pense que c'est l'une des plus belles fonctionnalités du langage. Imaginez que vous ayez besoin d'écrire une fonction qui ouvre trois fichiers. Et si quelque chose échoue, vous devrez fermer les fichiers ouverts existants. S'il y a beaucoup de constructions comme ça, ça ressemblera à un gâchis. Considérez cet exemple de pseudo-code :
function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
Ça a l'air compliqué. C'est là que le defer
de Go entre en place :
package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
Comme vous le voyez, si nous obtenons une erreur lors de l'ouverture du fichier numéro trois, les autres fichiers seront automatiquement fermés, car les instructions de defer
sont exécutées avant le retour dans l'ordre inverse. De plus, il est agréable d'avoir l'ouverture et la fermeture de fichiers au même endroit au lieu de différentes parties d'une fonction.
Conclusion
Je n'ai pas mentionné toutes les bonnes et mauvaises choses de Go, juste celles que je considère comme les meilleures et les pires choses.
Go est vraiment l'un des langages de programmation intéressants actuellement utilisés, et il a vraiment du potentiel. Il nous fournit des outils et des fonctionnalités vraiment sympas. Cependant, il y a beaucoup de choses qui peuvent être améliorées là-bas.
Si nous, en tant que développeurs Go, mettons en œuvre ces changements, cela profitera beaucoup à notre communauté, car cela rendra la programmation avec Go beaucoup plus agréable.
En attendant, si vous essayez d'améliorer vos tests avec Go, essayez Testing Your Go App: Get Started the Right Way par son collègue Toptaler Gabriel Aszalos.