23 ポイント 投稿者 GN⁺ 2025-12-31 | 1件のコメント | WhatsAppで共有
  • Goファイルを実行ファイルのように直接実行できるトリックを紹介
  • 1行目に //usr/local/go/bin/go run "$0" "$@"; exit を置き、実行権限を与えると ./script.go で実行可能
  • この方式は shebang ではなく、POSIXで ENOEXEC が発生した際にシェルが /bin/sh にフォールバックする動作を利用している
  • シェルは1行目をコマンドとして実行し、Goコンパイラは // コメントとして認識して無視する
  • "$0" で自分自身のパスを渡し、go run がスクリプトをビルド・実行し、$@ で引数を渡す
  • Goの強力な標準ライブラリと後方互換性の保証はスクリプティング用途に適しており、Go 1.x を使う限りスクリプトは数十年にわたって動作可能
  • Pythonの仮想環境、pip/poetry/uv など依存関係管理の複雑さを避けられる

偽のshebangの仕組み

  • shebang(#!)は execve システムコールを通じてインタプリタを指定する方式だが、この記事で紹介する手法は shebang ではない
  • Goソースファイルの1行目に //usr/local/go/bin/go run "$0" "$@"; exit を置き、package main 以下に通常のGoコードを書く形
    • chmod +x script.go で実行権限を与えると ./script.go のように実行できる
  • strace で確認すると、シェルが ./script.go を execve で実行しようとした際に、カーネルが ENOEXEC(Exec format error)を返す
    • ENOEXEC を受け取ったシェルは、フォールバックとして /bin/sh を使ってそのファイルをシェルスクリプトとして解釈する
    • シェルでは // はコメントではなくルートパス(/)として解釈されるため、//usr/local/go/bin/go は正常なパスとして実行される
  • そのため1行目の //usr/local/go/bin/go run "$0" "$@"; exit がシェルでコマンドとして実行される
    • "$0" は実行されたファイルのパスを渡すため、実行時には "$0"script.go のパスとなり、go run自分自身を見つけてビルド・実行する形になる
    • "$@" は1番目以降の位置引数展開で、./script.go -f flag0 here are some args のような呼び出しを可能にする
    • ; exit がないと、sh が Go ファイルをそのまま行単位で解釈し続け、package のようなトークンでエラーになる

Goがスクリプティングに向いている理由

  • Goの後方互換性の保証が中核的な特徴で、Go 1.x を使う限り作成したスクリプトは長期間動作する
  • 充実した標準ライブラリと組み込みツール(フォーマッタ、リンタなど)が追加設定なしで提供され、スクリプトの共有と移植性が最大化される
    • Pythonのように仮想環境や複数のパッケージマネージャ(pip, poetry, uv)を学ばなくてもコードを実行できる
    • Goエコシステムの組み込みツールとIDE連携により、.pyprojectpackage.json がなくてもフォーマッタやリンタを標準で使える
  • 最新のGoさえインストールされていれば、どのOSでも数十年にわたって実行可能

他のコンパイル言語との比較

  • Rust はコンパイル速度が遅く、標準ライブラリが弱いため依存関係の利用がほぼ必須で、完全性を求めるぶん開発速度も遅くなりがち
  • JavaおよびJVM言語にはすでにJVMバイトコードベースのスクリプティング言語が存在し、軽量なKotlinスクリプティングも代替になり得る
  • Goはコンパイル言語の中でもスクリプティング用途に最も適した特性を備えている

gopls のフォーマット問題と解決策

  • gopls はコメントの後ろに空白を要求する(//example// example)ため、偽のshebang行が壊れてしまう
  • 空白が入ると // usr/local/go/bin/go となり、シェルでパスとして認識されない
  • 解決策: HNスレッドの提案として // の代わりに /**/ ブロックコメントを使う
    • /*usr/local/go/bin/go run "$0" "$@"; exit; */ の形で書く
    • exit の後ろにセミコロン(;)が必須

1件のコメント

 
GN⁺ 2025-12-31
Hacker Newsのコメント
  • 筆者が言う「pip vs poetry vs uv を気にしたくない」という部分は、実は uv がこのユースケースを直接サポートしている
    PyPI 依存関係まで含めて、Python のバージョンと uv さえ入っていればよい
    uv 公式ドキュメントへのリンク

    • これよりさらに良い方法もある
      #!/usr/bin/env -S uv run --python 3.14 --script
      こうすると Python 自体がインストールされていなくても、uv が指定バージョンをダウンロードして実行する
    • 私もそう思ったが、Python 非ユーザーにとってはまだ直感的ではない
      Clojure を初めて触るとたいてい Leiningen を使えと勧められるが、Python は検索すると venv、poetry、hatch、uv などいろいろ出てくる
      uv は徐々にデファクトになりつつあるが、まだ一般的とは言えない
      以前 Go を apt で入れたらバージョンが古すぎて入れ直したことがあるが、それはずっと早く解決できた
      Python の仮想環境の問題は依然として複雑だ
    • 私は 2019 年に PyFlow でこの問題を解決していた
      Rust で書かれた OSS ツールで、Python のバージョンと venv を自動管理する
      pyproject.toml だけ設定して pyflow main.py を実行すれば、Cargo のように依存関係をインストールしてロックし、プロジェクトに合った Python バージョンにも自動で合わせてくれる
      当時は Poetry と Pipenv が人気だったが、venv やバージョン管理までは不十分だった
    • 私もほとんど uv に移行した
      主に uv add を使い、必要なときだけ uv pip を使う
      ただし uv pip は pip の限界をそのまま引き継いでいる — インストール順によって依存関係の解決結果が変わる
      uv pip install dep-a の後に dep-b を入れるのと、順番を逆にするのと、一度に入れるのでは全部違う
      これは pip 側の問題に近いが、Python パッケージ管理の混乱は依然として残っている
    • 実は Python のバージョンすら指定する必要はない
      uv が勝手にダウンロードしてくれる
  • Go は shebang サポートを明示的に拒否している
    代わりに gorun を使うことが推奨される
    /// 2>/dev/null ; gorun "$0" "$@" ; exit $? のような POSIX トリックで実行できる
    Nim、Zig、D は -run オプションで似たように使え、Swift、OCaml、Haskell はファイルを直接実行できる
    関連議論へのリンク

    • 小さなスクリプトなら go run より yaegi インタープリタのほうが良いかもしれない
      yaegi GitHub
  • 「pip、poetry、uv の違いなんて知りたくない、ただコードを実行したい」という話は、結局のところ 技術習熟度の問題
    uv runPEP 723 がすでにすべての問題を解決している

    • そう、でも uv run が出てくるまでに時間がかかりすぎた
      20 年以上 Python を使ってきたが、外部パッケージや venv のあるコードベースはいつも怖かった
      uv run のおかげで会社のプロジェクトはすべて移行したが、個人プロジェクトはすでに Go に移っていた
      長期的には 静的型付け言語を好む
    • 歴史のある言語なら、結局は競合するライブラリ群を学ぶことになる
    • これは UX の問題
      ユーザーはただプログラムが動いてほしいだけだ
      uv run と PEP 723 が問題を解決したとはいえ、uv を知っている必要があるという点で、依然として参入障壁がある
      uv が公式のデフォルトツールでない限り、多くのユーザーは Python を離れていくだろう
  • 本当に 天才的なアイデアだと思う
    ただ、スクリプティングには配布用ソフトウェアとは異なるエルゴノミクスの感覚が必要だ
    bash は即興的で、Go は製品化向き、Python はその中間くらい、Ruby は bash 寄りで、Rust は Go 寄りだ
    スクリプトは OS コマンドを素早く組み合わせて一回限りの作業を処理するときに有用だ
    Go にはそうした即興性が足りない

    • 私も Python が「中間くらい」という表現には共感する
      Debian で簡単な gtk アプリを uv で実行しようとしたが、依存関係は全部合っているのに起動せず、結局 Core Dump した
      Python を新しく試すたびにこういうことが起きる
      Go は冗長だが、一度コンパイルされればとにかく動く
    • 私も似たように感じる
      核心は 1 ファイルで完結できるかどうかだ
      Go でも 500 行くらいのスクリプトは書けるが、言語自体が複数ファイルとモジュールを前提にしている
      bang-line が使えないのもそのためだ
      どうせ go run が一時バイナリを作る仕組みなら、そのままビルドして /usr/local/bin に置くほうがよいと思う
    • bash が OS コマンドにより近いというのは誤解だ
      bash も Python と同じくらい OS の上にある抽象レイヤーにすぎず、ただ標準シェルだからそう感じるだけだ
    • LLM がコードを代わりに修正してくれる時代なら、書きやすさより可読性のほうが重要になるかもしれない
      特に LLM が書いたコードを人間が読みやすくする方向で
  • Python を初めて触るユーザーが pip、poetry、uv の違いを知らなくてよいという点には同意する
    ただ、こういうテーマで記事を書くブロガーなら、少なくとも uv が問題を解決することくらいは知っているべきだ
    無知な批判には説得力がない

    • uv が Go のような「write once, run anywhere」を解決するのか、という疑問がある
      私も uv の概念を完全には理解していないので気になる
  • 私は Python でスクリプトを書くのが好きだ
    素早く作業できるし、型やメモリを気にせず単純なことを処理するのに向いている
    ただし大規模アプリケーションには使いたくない

    • 私も Python スクリプティングは好きだが、他人のスクリプトをインストールするのは嫌だ
    • これは Linux 中心の見方だ
      多くのシステムにはデフォルトで Python が入っているので、簡単なスクリプトには十分だ
      Go をインストールしなければならないことを考えると、むしろ uv で Python を使うほうがましだと思う
      筆者自身も「少しトローリング気味に始めた」と言っていたように、結局は Go 好みの問題だ
    • JS もスクリプト言語として悪くないと思う
      node bla.js で終わりだ
    • 型は常に気にする必要がある
      関数が何を返すかは知っていなければならず、言語に慣れていれば基本的な型は記憶で処理する
      これは静的型付け言語でも同じだ
    • Python は開発者にとっては素晴らしいが、配布や統合には悪夢
      他人のことを考えるなら、Python で配布用コードを書くべきではない
  • 期待していたのは Python 批判だったのに、むしろ役立つヒントだった
    // をコメントとして使う言語なら、このトリックを応用できる
    C/C++、Java、JavaScript、Rust、Swift、Kotlin、ObjC、D、F#、GLSL などで可能だ
    特に GLSL で単一ファイルのグラフィックデモを作るのは面白い
    Shadertoy の例
    C ではブロックコメントを使って /*/../usr/bin/env gcc "$0" "$@"; ./a.out; rm -vf a.out; exit; */ のような方法も使える

    • Swift には外部依存関係のあるスクリプトを実行できる swift-sh プロジェクトがある
      Swift 版 uv のようなものだ
      Swift は shebang も公式サポートしている
    • C/C++ では #! をそのまま書いてもよい
      昔の TCC 時代には「C スクリプティング」としてこういう方法を使っていた
      大きなプロジェクトでは、ビルドスクリプトがマニフェストを読んでビルド後に実行する構成だった
      ただ、環境制御が難しいので実務には向かない
    • Rust はこんな小細工をする必要がない
      shebang を直接サポートしている
  • もっとエルゴノミクスの良い言語が欲しいなら、.NET 10 の「run file directly」機能もある
    shebang をサポートし、スクリプト内でパッケージを自動インストールする
    #:sdk ディレクティブで Web アプリもそのまま実行できる

    • 私も今日この機能で初めて C# スクリプトを書いたが、かなり良い体験だった
      ただし AOT コンパイルはまだ荒削りだ
  • 最初は Python 批判かと思ったが、むしろ言語エコシステムの方向性を考えさせられた
    ML が Python に縛られたのは大きな失敗だったと思う
    遅いし、型システムは扱いにくく、配布も難しいからだ
    いまは TypeScript、Go、Rust のような代替も考えるべきだ

    • 同意する
      ただし ML が Python を選んだ理由は C ベースの FFI にある
      NodeJS や Rust、Go は FFI が弱い
      Python はそこに強みがある
      理想は Python のように簡単だが、より良い型システムと配布体系を持つ言語だ
    • TypeScript で置き換えるというのには賛成できない
      JS エコシステムから出てきた言語で Python を置き換えたくはない
    • ML が Python に行ったのは市場圧力のためだ
      Lisp や Lua(Torch)のほうが適していたが、単純さゆえに Python が選ばれた
      私も Lisp ベースの ML フレームワークを開発中だが、採用は難しそうだ
    • Python の依存関係地獄は今も深刻だ
      バージョン互換性の問題、semver の不在、不安定なエコシステムなどのせいで、JS より後れているように感じる
      JS/Node はこの 10 年で成熟したが、Python はいまだに 2012 年に留まっている
      ML が Python に標準化されたのは本当に残念だ
    • 私は単純で表現力があり、強い型付け + ネイティブコンパイルされる言語を求めている
      CLI ツールを作るときは Python より Go のほうがずっと速い
      LOC の差のせいで Python に戻ってはいるが、実行するたびに Go が恋しくなる
      おそらく OCaml が理想だが、古いツール群が負担だ
  • Go スクリプトの問題は、1 行目に空白があってはいけないことだ
    gopls が自動フォーマットを強制するためだ
    CI でもフォーマットの一貫性を保つ必要があるので、これは実務上重要だ
    しかしもっと大きな問題は go.mod が使えないこと
    つまり依存関係のバージョンを指定できず、互換性の保証が弱くなる

    • それでもメジャーバージョンは import パスで固定されるので、基本的には互換だ
    • これは言語/ランタイムレベルの互換性の問題であって、依存関係の問題ではない