2023/04/11

Goのリポジトリ名とディレクトリ名とファイル名とパッケージ名の関係

Golangプロジェクトのリポジトリ名とディレクトリ名とファイル名とパッケージ名の関係を完全理解したので愛しく切なく心強いメモを書いておく。

お仕事でやってるプロジェクトだと、ゼロから自分で書くことがほぼなかったのとボイラープレートが用意されていたりしてこの辺の理解が薄かった。自分でイチからライブラリやアプリケーションを書いてみたいなと思ったとき、はじめていろいろ自分で手を動かして試して理解した内容を記します。いまさら当然のことしか書いてないので、そういう突っ込みはしないで欲しい。春のgolangおさらい祭り。

Golangプロジェクトのリポジトリ名

先に結論。

  • Golangプロジェクトのリポジトリ名は自由
  • リポジトリ名と package が同じであることが多いが、同じでなくて問題ない
  • 鉄の掟は、ひとつのディレクトリにひとつのパッケージ(テスト除く)

というわけで、まず、Golangプロジェクトを理解する前提として、プロジェクトがどういうものかを知る必要がある。Golangプロジェクトは大きく下の3つに分類できる。

  1. アプリケーション
  2. ライブラリ
  3. アプリケーションでありライブラリでもある

この種別は自明なことかもしれないが、golangプロジェクトを読むにも書くにも、リポジトリを設定したりディレクトリ構造を考えたりするうえで第一に重要な判断事項になるので、強めに意識した方が良い。

ひとつずつどういうものか書く。

アプリケーション

「アプリケーション」は、CLI/TUIアプリケーションだったりjobを粛々とこなす常駐ワーカーアプリケーションだったり、それがいわゆるWebサーバアプリケーションだったり、一般的にソフトウェアとして動作するものを指す。そうしたアプリケーションを伴うgolangプロジェクトは、package main が存在する。golangのチュートリアルによく出てくる "Hello, world!" を main.go で出力するようなものも言ってしまえばアプリケーション。

ライブラリ

つぎに「ライブラリ」に該当するプロジェクトは 文字通り、他のgolangプロジェクトからimportされるpackageを主体とするもの。基本的に package main は存在しない。ライブラリとしての package が存在する。(ライブラリを組み込んだ参考アプリケーションが同梱されているような場合は、そのアプリケーションをビルドすることはあるかもしれないが)

アプリケーションでありライブラリでもある

ひとつのプロジェクトの中にアプリケーションもあり、外部からimportされるライブラリも存在するようなものもある。CLIアプリケーションによくあるパターン。ライブラリ部分は import される想定で、アプリケーション部分は package main が存在する。

以上のように、golangプロジェクトはおおむね上記の3種のどれかに該当する。とりわけ、package main の存在は、アプリケーションかライブラリかの分かれ目なので、そこは強く意識した方が良い。

そして、golangプロジェクトのリポジトリ名とパッケージ名は同一であることが多いが、実は何でもよい。鉄則は、ひとつのディレクトリに存在できる package はひとつだけ(テストは除く)、ということ。だから、アプリケーションとライブラリが混在するとき、package main とライブラリ用の package something のディレクトリは必ず分かれることになる。

アプリケーションとライブラリがそれぞれひとつずつの場合、レイアウトは以下の3つのどれかになる。

  • リポジトリルートに main を置いてサブディレクトリにライブラリ package something
  • リポジトリルートにライブラリ package something を置いてサブディレクトリに main
  • リポジトリルートに何も置かず、それぞれにサブディレクトリを切って mainpackage something

Golangリポジトリのディレクトリ名

というわけで、Golang リポジトリファイルレイアウトは、慣例的なものであり、golang言語的な縛りは多くない。go {run|test|build} コマンドもターゲットとなるディレクトリやファイルについて比較的自由度の高い指定の仕方ができる。ここではその詳細を割愛しますが、とにかくリポジトリ内のディレクトリ・ファイルレイアウトは慣例的なもので、こうしなければいけないという決まったルールは少ない(go rungo build が動く限り)。重要なのはここでもやっぱり ひとつのディレクトリに存在できる package はひとつだけ、ということとなる。

  • ひとつのディレクトリにはひとつの package しかレイアウトできない
  • ディレクトリ名とパッケージ名は違っていても大丈夫
  • ひとつのリポジトリに複数の package something (つまり複数のディレクトリ)があってももちろんOK
  • ひとつのリポジトリに複数の package main (もちろんディレクトリは別々)があってももちろんOK

ちなみに、main.go をビルドしたときに生成する実行バイナリのファイル名を明示しない場合、main.go のあるディレクトリ名が実行バイナリ名になる。この挙動に依存して、main.go を置くディレクトリ名をコマンド名代わりに掘ることが慣例となっているが、本当に慣例であってルールではない(ビルド時にバイナリ名を指定する必要があると CIでビルドする場合にworkflowの中にハードコードする必要が出てくるのとかも面倒くさいのでそれを避けてるのではないかと予想してる)。

Golang の .go ファイル名

Golang の .go ファイル名は、テストコードに該当するものは *_test.go という風にする。また、_. ではじまるファイルはビルド対象から外れる。それ以外は自由。とにかく、ファイル名は自由で良いが .go ファイル内に書かれるパッケージは同じディレクトリなら全部同じである必要があり、その制約は必ず守る必要がある(テストコードでは例外がある)。また、ビルド時にプラットフォームごとにビルド対象を絞るための命名もある(*windows.go とか *linux.go とか)

ファイル名についてのまとめ。

  • *.go ファイルの置かれるディレクトリ名とファイル名は同じでなくて良い(小さいプロジェクトで同じになっているものがあるだけ)
  • *.go ファイル名はパッケージ名と同じでなくて良い
  • 同じディレクトリに *.go ファイルはたくさんあって良いがパッケージは同じである必要がある
  • テストファイルは *_test.go というファイル名になる
  • ファイル名でプラットフォームごとのビルド用にgoファイルを明示するルールがある *_windows.go

慣例的なリポジトリ内ディレクトリ/ファイル構成

というわけで、ここまで散々、本来リポジトリ名やディレクトリ名やファイル名やパッケージ名は基本自由だと書いてきたわけですが、現実的にgolangプロジェクトを書いていくにあたっては、慣例的なデファクトスタンダードというか、みんなこうしてるよねーとか、そのパターンならこういう感じ、という定石めいたものがある。

それらを書いておく。

全て github.com/user さんのリポジトリということにして、以下のような書式です。

reository/ ---- file.go `file.goのpackage`

一番シンプルなアプリケーションパターン

リポジトリ名が example で、リポジトリルートに main.go が置かれていて、中身は package main というパターン。Hello, world! するやつはこのパターンですね。

example/ ---- main.go `package main`

ちなみに、main.go のファイル名は何でも大丈夫。main.go じゃなくても良い。ただし、package main が必要で、func main() がエントリポイントになるのはルール。ファイル名は実は何でもよい。

$ go run main.go

という風で実行できる。ファイルを明示しないでディレクトリ指定でもいける。カレントディレクトリにmain.goがあるとして

$ go run .

という風でも実行できる。

packageが分割されているアプリケーションパターン

package main から分割して、package other をレイアウトするために、サブディレクトリ other を掘ってその中にファイルを置く。packageを分割するためにディレクトリを掘っているだけで、ディレクトリ名が other である必要はないし、その other の中に置かれるファイルが other.go である必要もない。実用的には other/ の中にたくさんのファイルが置かれるわけですし。必須なのは package other です。

repository/ --+- main.go `package main`
              |
              +- other/ ---- other.go `package other`

自由な命名でこんな感じでも大丈夫です。

repository/ --+- foo.go `package main`
              |
              +- bar/ ---- baz.go `package other`

package main のあるファイルが foo.go でも良い(func main() は必要)。そしてサブディレクトリとその中にあるファイルと packageも違っていて問題ありません。

一番シンプルなライブラリパターン

リポジトリ名が foo で、リポジトリルートに foo.go が置かれていて、中身は package foo というパターン。

foo/ ---- foo.go `package foo`

importしてもらうときは以下のように書く。

import "github.com/user/foo"

以下のように、リポジトリ名に go- というプリフィックス付きのもよくある。

go-foo ---- foo.go `package foo`

この場合でも、importしてもらうときは go-foo でインポートしてもらい、関数を呼ぶときは packageの foo で呼んでもらう。

以下は go-foo を利用する側の例。

package main

import "github.com/user/go-foo"

func main() {
    foo.SomeFunc() // package foo が SomeFunc() を実装しているとする
}

ちなみに、リポジトリ名が foo-go でも同じ。

foo-go/ ---- foo.go `package foo`

foo-go を利用するコードは

package main

import "github.com/user/foo-go"

func main() {
    foo.SomeFunc() // package foo が SomeFunc() を実装しているとする
}

importするときは純粋にパスで書いて、関数を呼ぶときはpackageを利用する。importパスの最後の文字列はなんでも良い。コード上は package で利用するから。

個人的には、パスの最後とpackageが異なる場合は、go-* プリフィックスのような比較的自明な場合も含めて、エイリアスで package を明示する方が良いのではないかなと思う。エイリアス自体はpackage名の重複を解決するために利用されることが多いと思うのだけど。

package main

import foo "github.com/user/foo-go"

func main() {
    foo.SomeFunc()
}

複数のpackageのあるライブラリ

う、ごめん、力尽きた。。。

ここから先は公式のガイドに従えばいいと思うですまる。

https://github.com/golang-standards/project-layout/blob/master/README_ja.md

あと、Go 1.18からWorkspace modeなるものがあるので、そのへんを使うのが良さそうだけどわたくしも勉強中!

おしまい

サイト内検索