- 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連携により、
.pyproject や package.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件のコメント
Hacker Newsのコメント
筆者が言う「pip vs poetry vs uv を気にしたくない」という部分は、実は uv がこのユースケースを直接サポートしている
PyPI 依存関係まで含めて、Python のバージョンと uv さえ入っていればよい
uv 公式ドキュメントへのリンク
#!/usr/bin/env -S uv run --python 3.14 --scriptこうすると Python 自体がインストールされていなくても、uv が指定バージョンをダウンロードして実行する
Clojure を初めて触るとたいてい Leiningen を使えと勧められるが、Python は検索すると venv、poetry、hatch、uv などいろいろ出てくる
uv は徐々にデファクトになりつつあるが、まだ一般的とは言えない
以前 Go を apt で入れたらバージョンが古すぎて入れ直したことがあるが、それはずっと早く解決できた
Python の仮想環境の問題は依然として複雑だ
Rust で書かれた OSS ツールで、Python のバージョンと venv を自動管理する
pyproject.tomlだけ設定してpyflow main.pyを実行すれば、Cargo のように依存関係をインストールしてロックし、プロジェクトに合った Python バージョンにも自動で合わせてくれる当時は Poetry と Pipenv が人気だったが、venv やバージョン管理までは不十分だった
主に
uv addを使い、必要なときだけuv pipを使うただし
uv pipは pip の限界をそのまま引き継いでいる — インストール順によって依存関係の解決結果が変わるuv pip install dep-aの後にdep-bを入れるのと、順番を逆にするのと、一度に入れるのでは全部違うこれは pip 側の問題に近いが、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 runと PEP 723 がすでにすべての問題を解決しているuv runが出てくるまでに時間がかかりすぎた20 年以上 Python を使ってきたが、外部パッケージや venv のあるコードベースはいつも怖かった
uv runのおかげで会社のプロジェクトはすべて移行したが、個人プロジェクトはすでに Go に移っていた長期的には 静的型付け言語を好む
ユーザーはただプログラムが動いてほしいだけだ
uv runと PEP 723 が問題を解決したとはいえ、uv を知っている必要があるという点で、依然として参入障壁があるuv が公式のデフォルトツールでない限り、多くのユーザーは Python を離れていくだろう
本当に 天才的なアイデアだと思う
ただ、スクリプティングには配布用ソフトウェアとは異なるエルゴノミクスの感覚が必要だ
bash は即興的で、Go は製品化向き、Python はその中間くらい、Ruby は bash 寄りで、Rust は Go 寄りだ
スクリプトは OS コマンドを素早く組み合わせて一回限りの作業を処理するときに有用だ
Go にはそうした即興性が足りない
Debian で簡単な gtk アプリを uv で実行しようとしたが、依存関係は全部合っているのに起動せず、結局 Core Dump した
Python を新しく試すたびにこういうことが起きる
Go は冗長だが、一度コンパイルされればとにかく動く
核心は 1 ファイルで完結できるかどうかだ
Go でも 500 行くらいのスクリプトは書けるが、言語自体が複数ファイルとモジュールを前提にしている
bang-line が使えないのもそのためだ
どうせ
go runが一時バイナリを作る仕組みなら、そのままビルドして/usr/local/binに置くほうがよいと思うbash も Python と同じくらい OS の上にある抽象レイヤーにすぎず、ただ標準シェルだからそう感じるだけだ
特に LLM が書いたコードを人間が読みやすくする方向で
Python を初めて触るユーザーが pip、poetry、uv の違いを知らなくてよいという点には同意する
ただ、こういうテーマで記事を書くブロガーなら、少なくとも uv が問題を解決することくらいは知っているべきだ
無知な批判には説得力がない
私も uv の概念を完全には理解していないので気になる
私は Python でスクリプトを書くのが好きだ
素早く作業できるし、型やメモリを気にせず単純なことを処理するのに向いている
ただし大規模アプリケーションには使いたくない
多くのシステムにはデフォルトで Python が入っているので、簡単なスクリプトには十分だ
Go をインストールしなければならないことを考えると、むしろ uv で Python を使うほうがましだと思う
筆者自身も「少しトローリング気味に始めた」と言っていたように、結局は Go 好みの問題だ
node bla.jsで終わりだ関数が何を返すかは知っていなければならず、言語に慣れていれば基本的な型は記憶で処理する
これは静的型付け言語でも同じだ
他人のことを考えるなら、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 版 uv のようなものだ
Swift は shebang も公式サポートしている
#!をそのまま書いてもよい昔の TCC 時代には「C スクリプティング」としてこういう方法を使っていた
大きなプロジェクトでは、ビルドスクリプトがマニフェストを読んでビルド後に実行する構成だった
ただ、環境制御が難しいので実務には向かない
shebang を直接サポートしている
もっとエルゴノミクスの良い言語が欲しいなら、.NET 10 の「run file directly」機能もある
shebang をサポートし、スクリプト内でパッケージを自動インストールする
#:sdkディレクティブで Web アプリもそのまま実行できるただし AOT コンパイルはまだ荒削りだ
最初は Python 批判かと思ったが、むしろ言語エコシステムの方向性を考えさせられた
ML が Python に縛られたのは大きな失敗だったと思う
遅いし、型システムは扱いにくく、配布も難しいからだ
いまは TypeScript、Go、Rust のような代替も考えるべきだ
ただし ML が Python を選んだ理由は C ベースの FFI にある
NodeJS や Rust、Go は FFI が弱い
Python はそこに強みがある
理想は Python のように簡単だが、より良い型システムと配布体系を持つ言語だ
JS エコシステムから出てきた言語で Python を置き換えたくはない
Lisp や Lua(Torch)のほうが適していたが、単純さゆえに Python が選ばれた
私も Lisp ベースの ML フレームワークを開発中だが、採用は難しそうだ
バージョン互換性の問題、semver の不在、不安定なエコシステムなどのせいで、JS より後れているように感じる
JS/Node はこの 10 年で成熟したが、Python はいまだに 2012 年に留まっている
ML が Python に標準化されたのは本当に残念だ
CLI ツールを作るときは Python より Go のほうがずっと速い
LOC の差のせいで Python に戻ってはいるが、実行するたびに Go が恋しくなる
おそらく OCaml が理想だが、古いツール群が負担だ
Go スクリプトの問題は、1 行目に空白があってはいけないことだ
goplsが自動フォーマットを強制するためだCI でもフォーマットの一貫性を保つ必要があるので、これは実務上重要だ
しかしもっと大きな問題は go.mod が使えないことだ
つまり依存関係のバージョンを指定できず、互換性の保証が弱くなる