- Go est une option pour réduire la complexité excessive du développement backend, avec comme atouts majeurs une compilation rapide, le déploiement en binaire unique et une gestion stable des dépendances
- Au lieu d’abstractions complexes comme les décorateurs, métaclasses, macros, traits ou monades, Go adopte une conception de langage simple centrée sur les struct, les fonctions, les interfaces, les goroutines et les channels
- Avec uniquement la bibliothèque standard et les outils de base comme
embed, html/template, net/http, database/sql, encoding/json, go test et pprof, il est possible de gérer application web, base de données, tests, benchmarks et profiling
- Une goroutine est une unité d’exécution stackful d’un coût d’environ 2KB, et Go permet de traiter simplement la concurrence et la propagation de l’annulation via les channels,
sync.Mutex, le race detector et context.Context
- Le flux
go mod init, go build, scp, systemctl restart recommande un déploiement simple centré sur un unique binaire Go et Postgres, plutôt que node_modules, des configurations Docker·Kubernetes complexes ou des microservices excessifs
Pourquoi choisir Go
- Go est une option pour réduire la complexité excessive du développement backend, avec comme atouts majeurs la compilation rapide, le déploiement en binaire unique et une gestion stable des dépendances
- De la même manière que HTML est resté côté frontend une alternative à la complexification excessive, Go existe depuis plus de dix ans comme choix pour simplifier le backend
- Mobiliser une multitude de paquets Node, des outils de build TypeScript, Kubernetes, une équipe plateforme Rails ou même une réécriture en Rust pour un simple formulaire ou une application CRUD tournant à environ 40 requêtes par seconde est excessif
- Go privilégie le code lisible, les artefacts déployables et une faible charge opérationnelle plutôt que les « abstractions ingénieuses »
Une conception de langage volontairement ennuyeuse
- Si Go paraît ennuyeux, c’est intentionnel : le langage ne fournit pas d’abstractions complexes comme les décorateurs, métaclasses, macros, traits ou monades
- Ses éléments centraux se limitent essentiellement aux struct, aux fonctions, aux interfaces, aux goroutines et aux channels
- Il vise une simplicité telle qu’on peut lire la spécification rapidement et écrire du code productif le jour même
- Ce côté ennuyeux devient un avantage dans un codebase d’équipe
- Un junior arrivé le mois dernier peut lire du code écrit il y a deux ans par un principal
gofmt impose un format unique, ce qui réduit les débats sur le style de code
- Le langage lui-même rend difficile l’introduction d’abstractions excessivement complexes dans le codebase
La bibliothèque standard joue le rôle de framework
- Go permet de créer une application web sans framework dédié, uniquement avec la bibliothèque standard
- Avec
embed, html/template et net/http, on peut embarquer des templates HTML dans le binaire et construire une application qui les rend via des handlers HTTP
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
- Cet exemple est une application web fonctionnelle, et les templates HTML sont compilés puis inclus dans le binaire
- Sans
webpack, Vite, serveur de développement ni énorme node_modules, on peut construire avec go build puis déployer un seul fichier
- Les principales tâches backend peuvent être couvertes uniquement avec la bibliothèque standard et les outils de base
- Base de données :
database/sql
- JSON :
encoding/json
- Appels à d’autres services : client
net/http
- Exécution concurrente : mot-clé
go
- Tests :
go test
- Benchmarks :
go test -bench
- Profiling :
pprof
Une bibliothèque standard riche et profonde
-
io.Reader et io.Writer
io.Reader et io.Writer sont des interfaces avec une seule méthode chacune, mais elles servent de fondation essentielle à tout l’écosystème Go
- Il est possible, avec peu de code, de chaîner un corps de réponse HTTP vers un writer gzip puis vers un fichier disque
- Comme les principaux paquets partagent ces deux interfaces, on peut réutiliser les mêmes schémas à de nombreux endroits
-
context.Context
context.Context est la méthode standard pour propager l’annulation
- Si l’utilisateur ferme l’onglet du navigateur, le context de la requête est annulé, et la requête SQL ainsi que les appels HTTP en aval peuvent l’être aussi
- Pour éviter les fuites de goroutines ou les requêtes zombies qui épuisent le pool de connexions, il faut passer le context en premier argument et le respecter
-
Paquets d’encodage
encoding/json, encoding/xml, encoding/csv et encoding/binary font tous partie de la bibliothèque standard
- Leur usage est similaire grâce au schéma des tags de struct et au décodage via pointeurs, si bien qu’en en maîtrisant un, les autres deviennent faciles à utiliser
Un modèle de concurrence qui réduit la douleur
- Une goroutine n’est pas un thread OS en soi, mais une unité d’exécution stackful que le runtime multiplexe au-dessus des threads OS
- Le coût de démarrage d’une goroutine est d’environ 2KB, ce qui permet d’en créer 100 000 même sur un laptop
- Les channels agissent comme des conduits typés entre goroutines : un côté envoie, l’autre reçoit, et le runtime gère la synchronisation
- Lorsqu’un état partagé est nécessaire, on peut utiliser
sync.Mutex, et le race detector aide à trouver les data races
- Même un fetcher HTTP parallèle peut s’écrire sans bibliothèque dédiée, sans framework et sans rituel
async/await
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Exemple concret de route CRUD
- Une route de type CRUD qui lit des articles depuis Postgres et rend du HTML peut aussi rester assez simple pour tenir sur un seul écran
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
- Cet exemple montre au même endroit la base de données, les templates et le handler HTTP
- Comme
r.Context() est transmis à la requête SQL, la requête peut aussi être annulée si la connexion est fermée
- Sans ORM, conteneur DI, couche service ni dossier
controllers/ rempli de classes de base abstraites, on peut lire le code de haut en bas et comprendre son fonctionnement
Une gestion des dépendances qui ne ruine pas le week-end
- En démarrant un module avec
go mod init, les dépendances sont enregistrées dans go.mod et go.sum
go.sum constitue en pratique un registre cryptographique des éléments effectivement téléchargés, ce qui permet de vérifier si une dépendance différente de celle attendue est entrée dans le projet
- On évite la complexité des répertoires
node_modules, du drift de lockfile entre environnement de développement et CI, des peer dependencies, optional dependencies, devDependencies ou peerDependenciesMeta
- Si un build hors ligne est nécessaire,
go mod vendor télécharge les dépendances dans le répertoire vendor/, que la toolchain utilisera automatiquement
- On peut empaqueter tout le projet et ses dépendances dans une seule tarball, ce qui est avantageux pour l’exploitation et les revues de sécurité
Des outils fournis avec le compilateur
- Les outils de base de Go sont fournis sans plugins tiers ni fichiers de configuration séparés
gofmt standardise le formatage du code et réduit les débats de style ainsi que l’augmentation des diff due aux espaces
go vet sert à repérer les erreurs évidentes
go test exécute les tests
go test -race exécute les tests avec le race detector pour trouver les data races
go test -bench exécute les benchmarks
go test -cover permet de vérifier la couverture de tests
go tool pprof permet d’obtenir des flame graphs CPU et mémoire via l’endpoint HTTP d’un service de production en cours d’exécution
Le déploiement se résume à une commande de copie
- Le flux essentiel de déploiement Go consiste à construire le binaire, le copier sur le serveur puis l’exécuter
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
- Ce flux permet de déployer sans Dockerfile, multi-stage build, alertes de CVE sur l’image de base, manifestes Kubernetes, chart Helm, ArgoCD, service mesh ni sidecar
- Un déploiement en production est possible avec un binaire à liaison statique d’environ 12MB et un fichier unit systemd d’environ 20 lignes
- Si Docker est vraiment nécessaire, il suffit de mettre le binaire Go dans une image
FROM scratch
Par contraste avec les frameworks
- Des frameworks comme Rails, Django, Express ou Next.js apportent chacun leur propre charge : procédure de déploiement, ORM, admin, middleware, avertissements npm, changements de conventions de routage, etc.
- Un binaire Go se compile puis s’exécute, avec l’avantage d’une stabilité qui lui permet encore de tourner cinq ans plus tard
- Face à des frameworks qui peuvent être abandonnés plus vite ou à des mainteneurs en burnout, le modèle d’exécution simple de Go ressort clairement
Un binaire Go unique plutôt que des microservices
- Les microservices ne devraient pas être le choix par défaut, et il vaut mieux commencer par écrire un monolithe
- La configuration recommandée est un seul binaire Go, un seul Postgres et, seulement si nécessaire, un seul Redis
- Il est possible de servir HTML et API JSON sur le même port et de faire tourner l’ensemble sur un VPS unique
- Comme Go a un faible coût par goroutine et gère bien la concurrence, il peut monter sans difficulté jusqu’à 10 000 requêtes par seconde
- Si un vrai besoin de séparation apparaît, on peut découper le monolithe Go en déplaçant des packages vers des dépôts distincts
- Les interfaces étant déjà là, le langage pousse naturellement vers une structure qui anticipe cette séparation
Génériques et gestion des erreurs
if err != nil n’est pas un bug, c’est une fonctionnalité
- Cette approche oblige à décider explicitement quoi faire à chaque point d’échec, sans masquer les erreurs
- L’imbrication de
try/catch ne fait pas disparaître les erreurs, elle peut simplement les cacher jusqu’à une panne en production
- Les génériques ont été introduits dans Go 1.18, et il suffit de les utiliser quand on en a besoin
Conclusion
- Il n’est pas forcément nécessaire d’avoir un framework, des microservices, une réécriture en Rust ou un nouveau mét framework JavaScript
- La recommandation est de suivre un flux simple : lancer
go mod init, écrire main.go, embarquer les templates puis compiler et déployer
- Le choix ennuyeux est le bon choix, et Go est ce choix
1 commentaires
Avis sur Lobste.rs
Je ne veux pas tirer sur le messager, mais ce style de blog est fatigant et puéril. Ça a peut-être fait rire au début, mais à force, l’agacement augmente de façon exponentielle
Cela dit, Go est bien. Je suis récemment passé d’un projet TypeScript à un projet Go, et ma santé mentale comme mon moral au travail s’améliorent rapidement
J’accepte l’idée que
if err != niln’est pas un bug mais une fonctionnalité, mais je pense quand même que c’est le plus gros défaut de Go. Avec des types somme (sum types), on aurait pu faire quelque chose de bien plus ergonomique sans dépendre d’assertions de type à l’exécutionSi on va écrire comme ça, autant avoir au moins des insultes un peu spirituelles
Vu les autres commentaires, ça a l’air d’être une opinion impopulaire, et je ne veux pas paraître brutal, mais je déteste vraiment Go
Go, c’est un langage avec une syntaxe pas trop mauvaise posé sur un runtime efficace pour la concurrence, et poussé par la force de Google dans l’écosystème. À part ça, je trouve ça affreux
Le plus gros problème, c’est qu’il semble conçu pour ignorer délibérément des décennies de recherche en conception de langages de programmation, voire même certaines pratiques de terrain. Il a bien fini par avoir des génériques, mais seulement des décennies plus tard
Je ne dis pas qu’il faut forcément des types dépendants partout, mais il y a tout de même un minimum. Go n’a presque rien de ce qu’un langage moderne devrait offrir pour modéliser les données, les invariants et la structure du code. Rust a une courbe d’apprentissage plus raide, mais il est bien meilleur sur ces points, et il n’est même pas nécessaire d’avoir un système de types aussi sophistiqué que Rust pour faire quelque chose de tout à fait correct. Si le temps de compilation est une inquiétude, on peut construire un système de types simple mais utile, rapide et expressif
Et
if err != nil, c’est selon moi la pire manière possible de tapisser le code de bruit de gestion d’erreurs. Je ne comprends pas cette allergie au côté Go envers les types somme. Sur ce point, même les exceptions Java sont meilleures. En pratique, comme le langage n’offre rien de mieux pour gérer les erreurs, les gens finissent par prendre le pire bricolage possible pour une fonctionnalitéSi le texte d’origine n’avait pas été aussi suffisant, je n’aurais même pas écrit ce commentaire. “Utilise juste X”, c’est idiot. Il faut prendre l’outil adapté au cas d’usage, confortable et productif. Si c’est Go, alors prends Go ; sinon, choisis autre chose
C’est particulièrement utile dans des organisations comme Google, où il y a des milliers de développeurs et où l’on peut rester peu de temps dans une équipe ou une entreprise
Dans ce contexte, surtout pour des développeurs peu mûrs, l’absence de système de types avancé devient même en partie un avantage. On n’a presque jamais besoin de penser aux types au-delà des types de base ou des structs. Le langage donne très peu d’outils pour modéliser les données, mais en contrepartie on peut écrire beaucoup de code sans trop réfléchir
Je ne pense pas que ce soit très bon au niveau de la justesse au niveau du langage. Mais dans les grandes organisations, on s’appuie davantage sur l’infrastructure autour : analyse de monorepo, CI/CD, tests canari, outils d’observabilité, etc. Cette infrastructure porte bien plus de charge que dans les petites structures
Moi aussi, j’aime assez Go pour cette faible charge cognitive. J’écris du code seulement de temps en temps sur certains projets et je ne suis pas plongé au quotidien dans un projet de long terme. Pouvoir revenir dans une base de code que je n’ai pas vue depuis un mois et y travailler en moins d’une heure, c’est un gros avantage. En revanche, si j’étais développeur à temps plein sur un projet complexe, je l’aimerais probablement moins
Je pense que les développeurs Go ont voulu se concentrer sur les fondamentaux, justement parce que les langages précédents et la communauté de la théorie des langages ont trop négligé ces fondamentaux. Les gens se focalisent sur le système de types le plus complet possible, mais plus un système de types devient complexe et expressif, plus le rendement diminue, et tout l’effort investi dans le système de types ne compensera jamais une gestion de paquets affreuse, des outils de build qui forcent l’équipe à apprendre un nouveau DSL, un système de documentation incapable de générer automatiquement des liens vers les infos de types ou la documentation des paquets tiers, une bibliothèque standard maigre, de graves problèmes de performance, l’absence de stratégie de compilation statique, des temps de build pénibles, une courbe d’apprentissage abrupte, un système de types punitif, une syntaxe difficile à lire ou une mauvaise intégration éditeur
Dire que Go n’a aucun moyen de modéliser les données est tout simplement faux. Dans n’importe quel langage, on peut modéliser les données et les invariants, et Go fournit tout de même assez de système de types pour imposer ce modèle
Rust est excellent, et c’est un bon choix si la vitesse d’itération n’est pas critique, si l’on déploie sur du bare metal ou si les exigences de correction et de performance sont très fortes. Mais comme choix par défaut pour le développement applicatif généraliste, surtout en équipe, ce n’est pas idéal. On tape souvent
if err != nil, mais personne n’a les frappes par seconde comme goulot d’étranglementDire que
if err != niln’est pas un bug mais une fonctionnalité, et que cela vous oblige à voir tous les points où quelque chose peut mal tourner, est fauxEn réalité, ça ne l’impose pas. Il est même plus facile d’ignorer l’erreur si on ne la vérifie pas soi-même
Pour la manière de traiter ou propager les erreurs, Rust reste un exemple brillant
Heureusement, sur tous les projets Go sur lesquels j’ai travaillé ces dernières années, on ajoutait golangci-lint par-dessus les vérifications statiques très limitées intégrées à Go. Franchement, ça devrait être obligatoire sur tous les projets Go
Je déteste vraiment cette mode d’écriture, mais je suis d’accord avec le fond de ce que l’article veut dire
La phrase sur l’absence de
node_modules“de la taille d’une Volkswagen” est vraie, mais ce n’est pas unnode_moduleslocal au projet : c’est juste un cache de paquets global dans~/gowc -l go.sumDès l’ouverture de la page, j’ai vu “Hey, dipshit.” et je l’ai refermée immédiatement
Ça a le même problème que la plupart des articles qui encensent un langage de programmation. Ils parlent moins de la qualité du langage actuel que de l’horreur de celui qu’ils utilisaient avant
L’auteur semble avoir beaucoup souffert avec Ruby et TypeScript, peut-être Python aussi, et Go lui a résolu ce problème. Mais comme je n’utilise ni Ruby ni TypeScript, l’article ne m’a pas vraiment parlé
J’ai l’impression d’avoir lu des dizaines de variations de ce texte au fil des ans. Utilisez Haskell, parce qu’il a un typage statique contrairement à Python et JavaScript. Utilisez Rust, parce qu’il se déploie en binaire unique contrairement à Perl et Erlang. Utilisez Elixir, parce qu’il a une vraie concurrence et des canaux contrairement à Ruby et Tcl
Je suis content que l’auteur ait trouvé le langage qui lui convient, mais je ne suivrai pas son conseil
La valeur zéro de Go m’a toujours semblé être un défaut. Je pense qu’il vaudrait mieux obliger l’utilisateur à expliciter une valeur par défaut. À part ça, pour un langage qui n’est pas OCaml, il est plutôt bon
boolmanquant devrait valoirtrueL’expérience de déploiement et de compilation est excellente, mais je déteste vraiment écrire dans ce langage lui-même. À chaque fois, c’est une mauvaise expérience. Est-ce qu’il existe d’autres langages avec une bonne expérience de déploiement sans être aussi contraignants que Go ?
Est-ce que je rate quelque chose avec Go ?
J’ai récemment déployé une petite application Rails et il fallait énormément de configuration, donc ça m’a clairement fait apprécier les avantages de Go
x86_64-unknown-linux-musl. Ça produit un binaire statique qui s’exécute tel quel sur n’importe quelle machine Linux 64 bits. Ensuite, je le transfère avecscpet je le lanceIl me reste encore à gérer l’attribution du port et le démarrage manuel, mais je compte régler ça avec un peu de magie systemd
Avec bundler, on peut créer un exécutable unique qu’on peut déposer sur une machine Linux d’une autre distribution ; même si Qt n’est pas installé, l’utilisateur n’a qu’à lancer l’exécutable pour que toute l’interface fonctionne
Il y a quand même une réserve pour les pilotes OpenGL. Ça reste faisable, mais c’est plus compliqué que “copier puis exécuter”
Je trouve que le plus gros problème, c’est qu’on affirme que Go a été conçu pour la concurrence, tout en intégrant des pointeurs bruts qu’on peut facilement partager par erreur
Le fait d’être ennuyeux en soi, ce n’est pas un problème, mais Go échoue de manière assez spectaculaire à être réellement un langage ennuyeux
On dit qu’il n’y a pas de décorateurs, mais il y a les struct tags et la réflexion. Il est difficile de comprendre comment ces éléments interagissent avant d’avoir exécuté le code
Les interfaces structurelles et la réflexion sont des sources inquiétantes de comportements qui changent à distance. Il suffit d’ajouter une mauvaise méthode à une struct pour modifier complètement le comportement d’une bibliothèque
Même du point de vue de la documentation, c’est étrange. Pourquoi ne pas vouloir rendre explicite qu’un type est censé satisfaire une interface donnée ?
Je ne comprends pas pourquoi les goroutines ne sont pas simplement appelées des threads
Et pourquoi les channels devraient-ils être une fonctionnalité du langage ? À mon avis, c’est parce qu’il a fallu 10 ans pour admettre que les génériques pouvaient servir à autre chose qu’à trois types environ
Si les channels font partie du runtime, c’est sans doute parce que le planificateur de goroutines doit les connaître pour pouvoir réveiller plus facilement la goroutine réceptrice quand un channel cesse d’être vide. J’imagine que cette approche était plus simple