Kavacode

Blog

Go : cette rigidité qui agace… et qui sauve les projets

  • go
  • backend
  • architecture
  • development
  • performance
Go peut donner une étrange impression de régression lorsqu’on vient du C ou du C++. Pourtant, ses contraintes ne sont pas un accident : elles constituent une grande partie de sa philosophie.

Si tu viens du C ou du C++, ton premier contact avec Go ressemble souvent à un petit choc culturel.

Tu as parfois l’impression qu’on t’a retiré une partie de tes outils de précision pour te forcer à assembler des briques beaucoup plus simples.

Pas de surcharge d’opérateurs, pas d’héritage de classes, pas de macros, une gestion des erreurs très explicite et un compilateur qui refuse même gentiment de garder tes imports inutilisés.

Ce n’est pas un accident.

Cette rigidité fait partie de la philosophie de Go.

1. Go est né d’un problème d’ingénierie à grande échelle

Go commence à prendre forme chez Google en septembre 2007, lorsque Robert Griesemer, Rob Pike et Ken Thompson réfléchissent à un nouveau langage.

Le contexte est celui de très grandes bases de code, principalement développées en C++ et Java, avec des systèmes de build devenus complexes et des temps de compilation importants.

L’objectif n’est donc pas simplement de créer un C++ plus moderne.

Les concepteurs cherchent notamment à améliorer :

  • la lisibilité du code ;
  • la vitesse de compilation ;
  • la gestion des dépendances ;
  • la concurrence ;
  • l’outillage ;
  • la maintenabilité de grandes bases de code.

La FAQ officielle de Go explique clairement cette volonté de réduire la complexité nécessaire à la construction de gros projets logiciels.

Go n’a donc jamais réellement cherché à maximiser le pouvoir expressif du développeur individuel.

Il cherche plutôt à limiter le nombre de façons différentes de résoudre le même problème.

Et lorsque des centaines de développeurs travaillent sur une même plateforme, cette nuance devient assez importante.

2. La lisibilité prime sur l’expressivité

Dans de nombreux langages, une nouvelle fonctionnalité est souvent considérée comme intéressante lorsqu’elle permet d’écrire moins de code ou de créer des abstractions plus élégantes.

Go prend régulièrement le problème dans l’autre sens.

Il préfère souvent une construction légèrement plus longue mais immédiatement compréhensible.

L’exemple le plus visible reste gofmt.

Le langage possède depuis ses débuts un formatteur officiel qui impose une présentation commune du code.

gofmt -w main.go

ou simplement :

go fmt ./...

Comme l’explique Andrew Gerrand sur le blog officiel de Go, l’objectif est autant de faciliter la lecture que de supprimer les débats sans fin sur l’indentation, les espaces ou la position des accolades.

Cette philosophie se retrouve dans le langage lui-même :

  • pas de surcharge d’opérateurs ;
  • pas d’héritage de classes ;
  • pas d’arguments par défaut ;
  • pas de macros préprocesseur ;
  • peu de syntaxe implicite ;
  • une convention de formatage commune.

Le résultat est parfois moins spectaculaire.

Mais le code d’un projet Go a tendance à conserver une certaine homogénéité, même lorsque plusieurs équipes interviennent dessus.

Le code est généralement lu beaucoup plus souvent qu’il n’est écrit.

Go a simplement décidé de construire une partie du langage autour de cette réalité.

3. Les erreurs restent volontairement visibles

C’est probablement l’un des points qui agace le plus lorsqu’on découvre Go :

result, err := doSomething()
if err != nil {
    return err
}

Puis encore :

data, err := loadData()
if err != nil {
    return err
}

Puis encore.

Et encore.

Go n’utilise pas les exceptions comme mécanisme principal de gestion des erreurs.

La FAQ officielle explique que les concepteurs considèrent que le modèle try/catch/finally peut rendre le flot de contrôle plus difficile à suivre lorsqu’il est utilisé pour des erreurs ordinaires.

Une erreur Go est donc généralement une valeur retournée par une fonction.

Elle fait partie de son contrat :

func LoadUser(id string) (*User, error)

L’inconvénient est évident : cela produit davantage de code.

L’avantage l’est aussi : le chemin d’erreur reste local et visible.

Il n’existe pas de saut implicite vers un gestionnaire situé plusieurs niveaux plus haut dans la pile d’appels.

Ce n’est pas particulièrement élégant.

Mais lorsqu’un service tourne depuis trois ans en production et qu’il faut comprendre pourquoi une opération peut échouer, l’élégance syntaxique descend assez rapidement dans la liste des priorités.

4. Composition plutôt qu’héritage

Go ne possède pas de système traditionnel de classes et d’héritage.

Il repose largement sur la composition et sur des interfaces satisfaites implicitement.

type Writer interface {
    Write([]byte) (int, error)
}

Un type n’a pas besoin de déclarer explicitement qu’il implémente Writer.

Il lui suffit de fournir la méthode correspondante.

func (f *File) Write(data []byte) (int, error) {
    // ...
}

Cette approche réduit fortement le couplage entre une abstraction et ses implémentations.

Une structure n’a pas besoin de connaître toutes les interfaces auxquelles elle pourrait correspondre.

Cela évite également de construire des hiérarchies d’héritage profondes dont personne n’ose plus modifier la classe située trois niveaux au-dessus parce que dix-sept comportements mystérieux en dépendent.

L’industrie du logiciel ayant déjà suffisamment expérimenté ce sport, ce n’est probablement pas une grande perte.

Gophers au travail

5. La concurrence fait partie du langage

Go a également été conçu alors que les processeurs multicœurs devenaient la norme.

Le langage fournit donc directement les goroutines :

go processJob(job)

Une goroutine n’est pas un thread système.

Le runtime Go multiplexe un grand nombre de goroutines sur un ensemble plus réduit de threads système.

La documentation officielle indique qu’une nouvelle goroutine ne nécessite initialement que quelques kilo-octets de pile, celle-ci pouvant ensuite grandir ou diminuer selon les besoins.

Les goroutines sont complétées par les channels :

messages := make(chan string)

go func() {
    messages <- "hello"
}()

msg := <-messages

La philosophie est résumée dans Effective Go :

Don’t communicate by sharing memory; share memory by communicating.

Les channels ne remplacent évidemment pas tous les mécanismes de synchronisation. Go fournit également des mutex et d’autres primitives dans le package sync.

Mais il propose un modèle particulièrement simple pour structurer de nombreux traitements concurrents.

6. Les abstractions arrivent lentement

L’histoire des génériques est probablement l’un des meilleurs exemples de la prudence de Go.

Pendant plus de dix ans, le langage n’en possédait tout simplement pas.

Les génériques n’ont été intégrés qu’avec Go 1.18 en mars 2022.

Ce n’était évidemment pas parce que les concepteurs ignoraient leur existence.

La FAQ de Go explique que le coût supplémentaire dans le système de types devait être justifié par un bénéfice suffisamment important.

Le résultat reste volontairement plus limité que les templates C++.

Ce qui résume assez bien la philosophie du langage :

une fonctionnalité n’entre pas dans Go simplement parce qu’elle est puissante.

Elle doit également rester compatible avec la simplicité globale du langage.

7. Des compromis très différents du C++

Axe C / C++ Go
Philosophie Contrôle et expressivité maximale Simplicité et prévisibilité
Gestion mémoire Manuelle / RAII / smart pointers Garbage collector
Erreurs Codes de retour et exceptions en C++ Valeurs de retour explicites
Polymorphisme Classes, héritage, templates Interfaces implicites et composition
Généricité Templates extrêmement puissants Type parameters volontairement encadrés
Concurrence Threads et bibliothèques Goroutines et channels intégrés
Outillage Écosystème composé de nombreux outils go build, test, fmt, vet…

Il ne s’agit pas de déterminer quel modèle est intrinsèquement supérieur.

Les deux langages optimisent simplement des problèmes différents.

C++ donne énormément de contrôle au développeur.

Go cherche à réduire le nombre de décisions qu’il doit prendre.

Gophers au travail

8. Pourquoi cette rigidité finit par payer

Au début, certaines contraintes donnent franchement l’impression de régresser.

Un import inutilisé bloque la compilation.

import "fmt"

Tu ne l’utilises plus ?

Le compilateur refuse de continuer.

La gestion des erreurs semble répétitive.

L’absence de certaines abstractions oblige parfois à écrire du code qui paraît trivialement explicite.

Mais après quelques mois sur une base de code importante, l’effet inverse apparaît.

Un service Go est généralement assez facile à parcourir.

La toolchain est standardisée :

go build
go test ./...
go fmt ./...
go vet ./...

Les dépendances sont décrites par les modules Go.

La compilation est conçue pour rester rapide.

Et pour de nombreux services ne dépendant pas de composants natifs via cgo, le résultat peut être distribué sous la forme d’un unique binaire particulièrement simple à déployer.

Cette homogénéité devient intéressante lorsque le projet grossit.

Encore plus lorsqu’un développeur qui n’a jamais travaillé sur un service doit intervenir dessus deux ans plus tard.

Conclusion

Lorsqu’on vient du C ou du C++, Go peut donner l’impression d’avoir volontairement retiré une partie des outils du langage.

C’est précisément ce qu’il a fait.

Il échange une partie de l’expressivité individuelle contre davantage de lisibilité, de cohérence et de prévisibilité collective.

Ce compromis n’est pas adapté à tous les logiciels.

Pour du développement système très bas niveau, des moteurs nécessitant un contrôle précis de la mémoire ou certains environnements temps réel, C et C++ conservent évidemment des avantages fondamentaux.

Mais pour construire des API, des services réseau, des outils d’infrastructure ou des systèmes distribués maintenus par plusieurs équipes, la simplicité volontaire de Go devient rapidement une force.

La rigidité de Go n’est pas vraiment un défaut du langage.

C’est une partie essentielle du produit.

Et après quelques années à maintenir des logiciels en production, on finit souvent par apprécier un langage qui préfère être légèrement ennuyeux aujourd’hui plutôt que terriblement intéressant à déboguer dimanche à trois heures du matin.


Contactez-nous
Contactez-nous

Vous souhaitez concevoir une architecture backend en Go, moderniser des services existants ou construire des API performantes et maintenables ? N’hésitez pas à nous contacter et parlons-en ensemble. 😊