1 points par GN⁺ 2 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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

 
GN⁺ 2 시간 전
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 != nil n’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écution

    • Je préfère encore ça aux textes IA qui ménagent tout le monde sans jamais prendre position
    • C’était amusant à lire, mais je n’en ai pas vu tant que ça de ce genre. Cela dit, je trouve plus drôle d’appeler quelqu’un “walnut” que “dipshit”
      Si on va écrire comme ça, autant avoir au moins des insultes un peu spirituelles
    • D’accord. Il y a un moyen de signaler ça ? Ça ne rentre dans aucune catégorie de signalement
  • 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

    • Je pense que la place de Go dans l’espace de conception, c’est de privilégier la simplicité pour les développeurs juniors travaillant sur de très grosses bases de code et dans de très grandes organisations, avant presque tout le reste. Du coup, même des développeurs peu expérimentés peuvent lire du code et le modifier localement sans accumuler beaucoup de contexte
      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
    • Dart aussi est un langage de Google qui ne donne pas l’impression d’ignorer des décennies de recherche, mais personne ne l’utilise en dehors de Flutter. Go, ça va encore
    • Cet article imite un format de mème agressif et suffisant. Donc c’était évident que ça allait provoquer les gens, et je trouve dommage, parce que le fond mériterait une vraie discussion plutôt qu’une guerre de flammes
      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’étranglement
    • En dehors de Rust, il n’y a presque aucun langage moderne avec ce genre de fonctionnalités. À moins de vouloir programmer en Gleam ou en Swift, mais si on en est à ce niveau de niche, autant utiliser Haskell
  • Dire que if err != nil n’est pas un bug mais une fonctionnalité, et que cela vous oblige à voir tous les points où quelque chose peut mal tourner, est faux
    En 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

    • Exact. Sans quelque chose comme errcheck, il est bien trop facile d’ignorer les erreurs, et c’est juste idiot. On devrait au minimum obliger à les rejeter explicitement
      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
    • Sur ce point, Swift est meilleur. Fonctionnellement, c’est le même modèle, mais Swift facilite davantage la propagation des erreurs entre bibliothèques différentes. Cela dit, c’est surtout une question de compromis et de choix de conception, pas vraiment de mieux ou de moins bien
  • 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 un node_modules local au projet : c’est juste un cache de paquets global dans ~/go

    • Et en plus, ça salit le répertoire personnel. Il n’y a même pas de point devant. Je ne comprends pas comment ça peut être acceptable
    • J’ai toujours envie de dire : avant de se moquer de la taille des arbres de dépendances dans d’autres écosystèmes, faites déjà un wc -l go.sum
  • Dè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

    • Il semble y avoir ici pas mal de gens qui pensent devoir vendre Go aux lecteurs de Lobsters. Chez certaines personnes, ça peut au contraire produire un effet repoussoir
  • 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

    • J’aime bien la valeur zéro, et je la trouve assez astucieuse. En revanche, l’absence de fonctionnalité pour définir des valeurs par défaut me manque vraiment. Par exemple, il est très difficile de sérialiser un objet JSON où un bool manquant devrait valoir true
  • L’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

    • J’ai récemment commencé à compiler des projets Rust en 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 avec scp et je le lance
      Il 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
    • Côté expérience de déploiement, on a eu un succès étonnamment grand en entreprise avec nix bundler. Pour donner le contexte, on développe une application GUI Qt6
      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

    • Les goroutines ne sont pas des threads, mais une abstraction plus légère qui tourne sur un pool de threads. C’est pour ça qu’on peut facilement en créer des milliers
      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
    • Les goroutines sont des green threads avec plusieurs outils supplémentaires autour