23 points par GN⁺ 2025-12-31 | 1 commentaires | Partager sur WhatsApp
  • Présentation d’une astuce permettant d’exécuter directement un fichier Go comme un exécutable
  • En plaçant //usr/local/go/bin/go run "$0" "$@"; exit sur la première ligne et en ajoutant les droits d’exécution, il devient possible de lancer le fichier avec ./script.go
  • Cette méthode n’utilise pas un shebang, mais le comportement de repli de POSIX vers /bin/sh lorsqu’un ENOEXEC survient
  • Le shell exécute la première ligne comme une commande, tandis que le compilateur Go l’ignore en la traitant comme un commentaire //
  • Avec "$0", le chemin du script lui-même est transmis, ce qui permet à go run de construire et d’exécuter le script, et "$@" sert à transmettre les arguments
  • La puissante bibliothèque standard de Go et sa garantie de compatibilité ascendante le rendent adapté au scripting ; tant qu’on utilise une version Go 1.x, le script peut continuer à fonctionner pendant des décennies
  • Cela permet d’éviter la complexité de la gestion des dépendances comme les environnements virtuels Python, pip/poetry/uv, etc.

Fonctionnement du faux shebang

  • Un shebang (#!) sert à désigner l’interpréteur via l’appel système execve, mais la technique présentée ici n’est pas un shebang
  • Le principe consiste à mettre //usr/local/go/bin/go run "$0" "$@"; exit sur la première ligne d’un fichier source Go, puis à écrire du code Go classique après package main
    • En donnant les droits d’exécution avec chmod +x script.go, on peut le lancer directement comme ./script.go
  • En observant avec strace, on voit que lorsque le shell tente d’exécuter ./script.go via execve, le noyau renvoie ENOEXEC (Exec format error)
    • Après réception de ENOEXEC, le shell utilise /bin/sh comme solution de repli pour interpréter le fichier comme un script shell
    • Dans le shell, // n’est pas un commentaire mais est interprété comme un chemin racine (/), donc //usr/local/go/bin/go est exécuté comme un chemin valide
  • La première ligne //usr/local/go/bin/go run "$0" "$@"; exit est donc exécutée comme une commande par le shell
    • "$0" transmet le chemin du fichier exécuté ; dans la commande, "$0" devient donc le chemin de script.go, ce qui permet à go run de retrouver, construire et exécuter son propre fichier source
    • "$@" développe les paramètres positionnels à partir du premier argument, ce qui permet des appels comme ./script.go -f flag0 here are some args
    • Sans ; exit, sh continuerait à interpréter le fichier Go ligne par ligne et finirait par produire une erreur sur des tokens comme package

Pourquoi Go est adapté au scripting

  • La garantie de compatibilité ascendante de Go est l’élément clé : tant qu’on utilise Go 1.x, un script écrit aujourd’hui peut fonctionner longtemps
  • Une bibliothèque standard mature et des outils intégrés (formatter, linter, etc.) sont fournis sans configuration supplémentaire, ce qui maximise le partage et la portabilité des scripts
    • Contrairement à Python, il n’est pas nécessaire d’apprendre les environnements virtuels ni divers gestionnaires de paquets (pip, poetry, uv) pour exécuter le code
    • Les outils intégrés de l’écosystème Go et leur intégration dans les IDE permettent d’utiliser formatter et linter par défaut, même sans .pyproject ni package.json
  • Dès lors que la dernière version de Go est installée, les scripts peuvent s’exécuter pendant des décennies sur n’importe quel OS

Comparaison avec d’autres langages compilés

  • Rust compile plus lentement, dispose d’une bibliothèque standard plus limitée, impose souvent l’usage de dépendances et sa recherche de perfection tend à ralentir le développement
  • Java et les langages de la JVM disposent déjà de langages de scripting basés sur le bytecode JVM, et le scripting Kotlin léger peut aussi être une alternative
  • Parmi les langages compilés, Go possède les caractéristiques les plus adaptées à un usage de scripting

Problème de formatage avec gopls et solution

  • gopls impose un espace après un commentaire (//example// example), ce qui casse la ligne de faux shebang
  • Si un espace est ajouté, // usr/local/go/bin/go n’est plus reconnu comme un chemin par le shell
  • Solution : utiliser, comme proposé dans le fil HN, un commentaire de bloc /**/ à la place de //
    • Sous la forme /*usr/local/go/bin/go run "$0" "$@"; exit; */
    • Le point-virgule (;) après exit est indispensable

1 commentaires

 
GN⁺ 2025-12-31
Réactions sur Hacker News
  • Le passage où l’auteur dit « je ne veux pas me soucier de pip vs poetry vs uv » est en fait un cas d’usage que uv prend directement en charge
    Y compris avec les dépendances PyPI, il suffit d’avoir la version de Python et uv installés
    Lien vers la documentation officielle de uv

    • Il y a même mieux
      #!/usr/bin/env -S uv run --python 3.14 --script
      De cette façon, même si Python lui-même n’est pas installé, uv télécharge la version demandée et l’exécute
    • C’est aussi ce que je pensais, mais pour les utilisateurs non-Python, ce n’est toujours pas très intuitif
      Quand on découvre Clojure, on entend généralement qu’il faut utiliser Leiningen, alors qu’avec Python, une recherche renvoie venv, poetry, hatch, uv, etc.
      uv est en train de devenir la solution dominante, mais ce n’est pas encore universel
      J’ai déjà installé Go via apt avant de me rendre compte que la version était trop ancienne et de devoir le réinstaller, mais ce problème s’est réglé bien plus vite
      La question des environnements virtuels Python reste compliquée
    • Moi, j’avais déjà résolu ce problème en 2019 avec PyFlow
      C’est un outil OSS écrit en Rust qui gère automatiquement la version de Python et le venv
      Il suffit de configurer pyproject.toml puis d’exécuter pyflow main.py pour qu’il installe et verrouille les dépendances comme Cargo, tout en ajustant automatiquement la bonne version de Python pour le projet
      À l’époque, Poetry et Pipenv étaient populaires, mais ils restaient insuffisants pour la gestion du venv et des versions
    • Moi aussi, je suis passé en grande partie à uv
      J’utilise surtout uv add, et seulement uv pip quand c’est nécessaire
      Mais uv pip conserve les limites de pip — la résolution des dépendances change selon l’ordre d’installation
      Installer dep-a avec uv pip install dep-a puis dep-b, inverser l’ordre, ou tout installer d’un coup, ce n’est pas la même chose
      C’est davantage un problème de pip, mais le chaos de la gestion des paquets Python reste bien présent
    • En réalité, il n’est même pas nécessaire de préciser la version de Python
      uv la télécharge automatiquement
  • Go a explicitement refusé la prise en charge du shebang
    À la place, il est recommandé d’utiliser gorun
    On peut l’exécuter avec une astuce POSIX comme /// 2>/dev/null ; gorun "$0" "$@" ; exit $?
    Nim, Zig et D peuvent faire quelque chose de similaire avec l’option -run, et Swift, OCaml et Haskell peuvent exécuter directement un fichier
    Lien vers la discussion associée

    • Pour les petits scripts, l’interpréteur yaegi peut être préférable à go run
      yaegi GitHub
  • Le propos « je ne veux pas connaître la différence entre pip, poetry et uv, je veux juste exécuter le code » relève au fond d’une question de maîtrise technique
    uv run et PEP 723 ont déjà résolu tout le problème

    • Oui, mais il a fallu beaucoup trop longtemps pour en arriver à uv run
      J’utilise Python depuis plus de 20 ans, mais les codebases avec paquets externes ou venv m’ont toujours fait peur
      Grâce à uv run, tous les projets de mon entreprise ont migré, mais pour mes projets personnels je suis déjà passé à Go
      À long terme, je préfère les langages à typage statique
    • Avec un langage ancien, on finit forcément par apprendre les bibliothèques concurrentes
    • C’est un problème d’UX
      Les utilisateurs veulent simplement que le programme s’exécute
      uv run et PEP 723 règlent le problème, mais il faut encore connaître uv, ce qui laisse une barrière à l’entrée
      Tant que uv ne sera pas l’outil officiel par défaut, beaucoup d’utilisateurs abandonneront Python
  • Je trouve que c’est une idée vraiment géniale
    Mais le scripting exige une ergonomie différente de celle d’un logiciel destiné au déploiement
    bash est improvisé, Go est adapté à la production, Python se situe quelque part entre les deux, Ruby est plus proche de bash, et Rust du côté de Go
    Les scripts sont utiles pour combiner rapidement des commandes OS et traiter des tâches ponctuelles
    Go manque de cette spontanéité

    • Je suis aussi d’accord avec l’idée que Python est « entre les deux »
      J’ai essayé de lancer une petite appli gtk sur Debian avec uv, toutes les dépendances semblaient correctes, et malgré cela l’exécution a échoué avec un Core Dump
      Ça se reproduit à chaque fois que je redonne sa chance à Python
      Go est verbeux, mais une fois compilé, ça fonctionne, tout simplement
    • J’ai un ressenti similaire
      Le point clé, c’est de savoir si tout peut tenir dans un seul fichier
      On peut écrire un script de 500 lignes en Go, mais le langage part du principe qu’il y aura plusieurs fichiers et des modules
      L’absence de bang-line va dans ce sens
      Puisque go run produit de toute façon un binaire temporaire, je trouve qu’il vaut mieux compiler directement et le mettre dans /usr/local/bin
    • Dire que bash est plus proche des commandes OS est une idée fausse
      bash est lui aussi une couche d’abstraction au-dessus de l’OS, autant que Python, on le ressent juste différemment parce que c’est le shell par défaut
    • À l’ère où les LLM modifient le code à notre place, la lisibilité pourrait devenir plus importante que l’ergonomie d’écriture
      Surtout si l’on veut que le code écrit par un LLM reste facile à lire pour des humains
  • Je suis d’accord sur le fait qu’un débutant en Python n’a pas besoin de connaître la différence entre pip, poetry et uv
    En revanche, pour un blogueur qui écrit sur ce sujet, il devrait au minimum savoir que uv résout ce problème
    Une critique fondée sur l’ignorance n’est pas convaincante

    • Une question revient : est-ce que uv résout le « write once, run anywhere » à la Go ?
      Comme je n’ai pas encore complètement compris le concept de uv, ça m’intéresse aussi
  • Moi, j’aime écrire des scripts en Python
    On peut travailler vite, et c’est pratique pour les tâches simples sans se soucier des types ni de la mémoire
    Mais je n’ai pas envie de l’utiliser pour de grosses applications

    • Moi aussi, j’aime le scripting en Python, mais je n’aime pas installer les scripts des autres
    • C’est une approche centrée sur Linux
      La plupart des systèmes ont déjà Python par défaut, ce qui suffit pour de petits scripts
      Si l’alternative est d’installer Go, je préfère encore utiliser Python avec uv
      Comme l’auteur l’a dit lui-même, « j’ai commencé un peu pour troller » ; au final, c’est surtout une question de préférence pour Go
    • Je pense aussi que JS n’est pas mauvais comme langage de script
      Avec node bla.js, c’est réglé
    • Il faut toujours se soucier des types
      Il faut savoir ce que renvoie une fonction, et quand on connaît bien le langage, on gère les types de base de mémoire
      C’est pareil dans les langages à typage statique
    • Python est excellent pour les développeurs, mais c’est un cauchemar pour le déploiement et l’intégration
      Si on tient compte des autres, il ne faut pas écrire en Python du code destiné à être distribué
  • Je m’attendais à une critique de Python, et au final c’était plutôt une astuce utile
    Si le langage utilise // pour les commentaires, on peut reprendre cette astuce
    C’est possible avec C/C++, Java, JavaScript, Rust, Swift, Kotlin, ObjC, D, F#, GLSL, etc.
    L’idée de faire une démo graphique en un seul fichier avec GLSL est particulièrement intéressante
    Exemple Shadertoy
    En C, on peut aussi utiliser un commentaire de bloc comme /*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */

    • Il existe pour Swift le projet swift-sh, qui permet d’exécuter des scripts avec des dépendances externes
      C’est un concept similaire à uv pour Swift
      Swift prend aussi officiellement en charge le shebang
    • En C/C++, on peut aussi écrire directement #!
      À l’époque de TCC, j’utilisais ce genre de méthode pour faire du « scripting en C »
      Sur les gros projets, le script de build lisait un manifeste puis construisait et exécutait le tout
      Mais comme il est difficile de contrôler l’environnement, ce n’est pas adapté à un usage professionnel
    • Rust n’a pas besoin de ce genre de bidouilles
      Il prend directement en charge le shebang
  • Si vous voulez un langage plus ergonomique, .NET 10 propose aussi la fonctionnalité « run file directly »
    Elle prend en charge le shebang et installe automatiquement les paquets dans le script
    Avec la directive #:sdk, on peut même exécuter immédiatement une web app

    • J’ai justement écrit aujourd’hui un script C# avec cette fonctionnalité pour la première fois, et l’expérience était plutôt bonne
      Cela dit, la compilation AOT reste encore un peu brute
  • Au départ, je pensais lire une critique de Python, mais cela m’a plutôt poussé à réfléchir à la direction prise par l’écosystème des langages
    À mes yeux, le fait que le ML soit lié à Python a été une grosse erreur
    C’est lent, le système de types est inconfortable, et le déploiement est difficile
    Il faut maintenant envisager des alternatives comme TypeScript, Go ou Rust

    • D’accord
      Cela dit, si le ML a choisi Python, c’est à cause de la FFI basée sur C
      NodeJS, Rust et Go sont plus faibles sur ce terrain
      Python a ici un vrai avantage
      L’idéal serait un langage aussi simple que Python, mais avec un meilleur système de types et un meilleur modèle de déploiement
    • Je ne suis pas d’accord avec l’idée de remplacer Python par TypeScript
      Je n’ai pas envie de remplacer Python par un langage issu de l’écosystème JS
    • Si le ML est parti sur Python, c’est à cause de la pression du marché
      Lisp ou Lua (Torch) étaient plus adaptés, mais Python a été choisi pour sa simplicité
      Je développe moi-même un framework ML basé sur Lisp, mais je doute qu’il soit largement adopté
    • L’enfer des dépendances en Python reste très sérieux
      Problèmes de compatibilité de versions, absence de semver, écosystème instable… on a l’impression que Python est en retard même par rapport à JS
      JS/Node a mûri ces dix dernières années, alors que Python semble toujours bloqué en 2012
      Le fait que le ML se soit standardisé sur Python est vraiment regrettable
    • Je veux un langage simple et expressif, avec typage fort + compilation native
      Pour créer des outils CLI, Go est bien plus rapide que Python
      Je reviens à Python à cause de la différence en nombre de lignes, mais Go me manque à chaque exécution
      OCaml serait probablement idéal, mais son outillage vieillissant me freine
  • Le problème des scripts Go, c’est que la première ligne ne doit contenir aucun espace
    Parce que gopls impose le formatage automatique
    Et comme il faut aussi garder un format cohérent en CI, c’est important en pratique
    Mais le vrai problème, plus grave encore, c’est qu’on ne peut pas utiliser go.mod
    Autrement dit, on ne peut pas fixer les versions des dépendances, donc les garanties de compatibilité sont plus faibles

    • Malgré tout, les versions majeures sont verrouillées dans les chemins d’import, donc dans l’ensemble ça reste compatible
    • C’est un problème de compatibilité au niveau du langage/runtime, pas vraiment un problème de dépendances en tant que tel