Goでパッケージ名とコマンド名を同じにしたい場合に

微妙にGoのディレクト命名規則がイケていない気がしてならない。

先日も書いたがGoのディレクトリ名とパッケージ名の間には以下の規則が存在。

  1. パッケージ名とディレクトリ名は同じにする
  2. 実行ファイルを作成する場合のみ、例外としてパッケージ名はmainにしなければならない
  3. 同一ディレクトリ内のファイルに異なるパッケージ名の宣言は認められない

ここで実行ファイルの命名規則がさらに微妙で、

  • 実行ファイル名はディレクトリ名から取得される。ファイル名ではない

つまり、test.goにpackage mainとfunc main()がある場合、go buildの結果はtestではなくディレクトリ名になる。どうしてこうなった?

偉大なるC言語様は実行ファイル名がa.outになるというこれまた妙な仕様だった。しかし偉大なるmakeコマンドのdefaultの挙動がCのソースのファイル名から実行ファイルを作成した。つまり、test.cをmake testすれば実行ファイル、testが得られたのだ。

それに比べてGoが微妙なのはファイル名の意味が無いことだ。ディレクトリ名から取るならファイル名は何でも良いことになって意味が無い。

go buildには-oオプションが存在し、オブジェクト名を変更可能だ。これを用いてディレクトリ名と異なる実行ファイルをビルドすることも可能だ。しかし、go installには-oオプションが無いというこれまた微妙な仕様が過去に問題になりながら今でも修正されていない。go getでも困ってしまう。

で、現状、自分が体験している問題はあるデータ構造とそれに関わるpackageを同じ名前にしつつ、かつコマンド名も同じにしたい。かつ、全部、同じディレクトリに入れたい場合である*1

例えばgzipを実装すると考えよう。gzipをtypeにしてpublicに公開したい。自分が普通に考えると、ディレクトリ名とpackage名と型名はgzipになる。そしてコマンド名もgzipにしたい。このとき、ソースを全部1つのディレクトリに入れることは命名規則からできない。githubに公開することを前提にするならリポジトリ名=ディレクトリ名はgzipにしたいだろう。ディレクトリ名がgzipなら使えるパッケージ名はgzipに限定される*2。しかしpackage名が異なるソースは同じディレクトリに入れられない。だからコマンドとしての、package mainなgzipは別ディレクトリに入れねばならない。しかし型gzipを扱うコマンドgzipのソースを別リポジトリに入れるのは何か嫌だ。するとディレクトリを掘るしかない。しかしディレクトリ名が実行ファイルになるのでgzipリポジトリの直下にgzipという名のディレクトリを掘ることになる。何か嫌だ。

まぁ、実際、多くのプロジェクトは実行ファイル向けにcmdやpkgという名のディレクトリを掘り、さらにその中に実行ファイル名のディレクトリを掘るということをしている模様だ*3

CLI ツール開発を支える技術 2019春 P24

https://speakerdeck.com/izumin5210/techniques-that-support-building-cli-tools-2019-spring?slide=24

これ、実行ファイル名をディレクトリ名でなく、ファイル名から取れば済むことじゃなかったのだろうか?

ディレクトリを多段にするとmakeも複雑で面倒になる。気になってGo言語自身はどうやってbuildしているのか見てみたらbashスクリプトのみでやっているようだ。

https://golang.org/src/

1つのディレクトリでは1つしか実行ファイルを作れないという強烈な制限はgo buildが複数のパッケージを処理する場合に、ディレクトリ名を複数指定するという規約に繋っている。筋は通っている。go buildに複数のgoファイルを指定する場合には1つのパッケージに属すると見做される。

何かあまりにも変で自分の理解が間違っている可能性が高いのだけれども、何か不愉快なので愚痴を書いてみた。

蛇足

ここまで書いて実際のgzipは型がGzipでなくReader、Writerなのに気付いた。Practical Goか何かで型名は衝突しても全然問題がない、なぜならGoではpackage名の省略は行わないからだと書いてあったのでそんなものなのだろう。Goではpackage名と型名の組で名前なのだ。自分も「郷に入っては郷に従う」をしたいと思う。

型名があまり重要でなく、package名が、つまりはディレクトリ名が重要なのがGo言語なのだろう。対してファイル名はあまり重要でないのかもしれない。それはそれで筋は通っているのだが、小さなプログラムを簡単に書きたいという需要には向いていない気がする。まぁ、dockerとかk8sとかでかいシステムを作るのがGo言語なのだからそんなことは最初から考えられていないのかも。やっぱりGoは初心者向けでないと感じる。

後、Practical Goではmainはできるだけ小さくせよと書かれている。その効用もわかるのだけれど、上記の制約から1つのディレクトリにぽつんと1つmainファイルが小さく存在する姿は妙なものだと感じてならない。従うけどね。

*1:恐らくこれが間違いだけど、納得の行く説明はまだ見つかっていない。多分、どっかで読み飛ばしているに違いない

*2:shouldなのでimportに苦労しても良いなら変えても良い

*3:もしくはmainファイルをトップに置いて、他のファイルは全てそれより下に置くか。ただこの場合には型を公開する場合、import文が長くなる