【翻訳】Goモジュールの構成(Organizing a Go module)

2023/09/24に公開

Go言語公式から、Organizing a Go module という、Goプロジェクトのファイル・ディレクトリ構成に関する記事が発表されました。
公式からGoプロジェクトの構成に関する指針が紹介されるのは(自分の知る限り)初めてのことだったので、精読がてら翻訳しました。

注意

訳文中では、「コマンドラインプログラム」あるいは単に「プログラム」と言った場合、コマンドを叩いて実行するプログラムを指します。

翻訳

「Go プロジェクトはどういうファイル・ディレクトリ構成にするべきか」は、多くのGo 入門者が抱く疑問です。このドキュメントの目的は、この疑問に対する指針を提供することです。Go Module の基本的な知識を持っていると、よりこのドキュメントを理解しやすいかと思います。Tutorial: Create a Go moduleManaging module source に説明があります。

このガイドラインでは、パッケージ、コマンドラインプログラム、またそれら2つが混在したプロジェクトなど、Go プロジェクトの種類ごとに指針を説明します。

パッケージの基本構成

基本的な Go パッケージでは、プロジェクトのルートディレクトリにすべてのコードを置き、モジュール・パッケージは1つだけで構成します。例えば、1つの.goファイルしかないパッケージでは、プロジェクト構成は以下のようになります。

project-root-directory/
  go.mod
  modname.go
  modname_test.go

(ドキュメントを通して、ファイル・パッケージ名は任意です)

このディレクトリをgithub.com/someuser/modnameというGitHubリポジトリにアップロードする場合、go.modファイルには

module github.com/someuser/modname

と書き、modname.goで以下のようにパッケージを宣言します。

package modname

// 以下パッケージのコード本体

このパッケージのユーザーは、以下のようにすることでパッケージを使用できます。

import "github.com/someuser/modname"

Go パッケージのコードは、同ディレクトリの別ファイルに分割できます。例えば、上のmodnameパッケージのコードは、以下のように分割してもよいです。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth.go
  auth_test.go
  hash.go
  hash_test.go

この場合、各.goファイルでpackage modnameと宣言することになります。

コマンドラインプログラムの基本構成

コマンドラインプログラムのディレクトリ構成は、コードの規模と複雑さによって変わります。簡単なプログラムなら、main関数を定義する.goファイル1つだけで十分です。大規模なプログラムでは、コードを複数ファイルに分割して、各ファイルでpackage mainと宣言します。

project-root-directory/
  go.mod
  auth.go
  auth_test.go
  client.go
  main.go

上のファイル構成では、main関数を含むのはmain.goですが、これは慣習的な命名です。main.goの代わりにmodname.goなど任意の名前を使用できます。

このディレクトリをgithub.com/someuser/modnameという GitHub リポジトリにアップロードする場合、go.modには以下のように記述します。

module github.com/someuser/modname

ユーザーは以下のコマンドでこのプログラムをローカルにインストールできます。

$ go install github.com/someuser/modname@latest

ユーティリティパッケージを含むパッケージ・コマンドラインプログラム

大規模なパッケージ・コマンドラインプログラムでは、一部の機能をユーティリティパッケージに分割することがあります。そういったパッケージはinternalディレクトリに配置することを推奨します。そうすることで、外部からの利用を想定していないパッケージに他の Go プロジェクトが依存してしまうことを防げます (参考)。他モジュールはinternalディレクトリのコードをimportできないので、APIなどを変更しても、外部のコードに影響が及ぶことはありません。この場合、ディレクトリ構成は以下のようになります。

project-root-directory/
  internal/
    auth/
      auth.go
      auth_test.go
    hash/
      hash.go
      hash_test.go
  go.mod
  modname.go
  modname_test.go

modname.goでは、package modnameauth.goでは、package authというように宣言します。modname.goは、以下のようにしてauthパッケージをインポートできます。

import "github.com/someuser/modname/internal/auth"

コマンドラインプログラムの場合も同様に、internalディレクトリにユーティリティパッケージを置きます。ただし、この場合はルートディレクトリのファイルはpackage mainと宣言します。

複数のパッケージを含むモジュール

1つのGo モジュールに、複数の公開パッケージがある場合です。この場合、1パッケージに1つのディレクトリを対応させます。ディレクトリ構成は階層状になっていてもよいです。例えば、以下のような構成が考えられます。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
    token/
      token.go
      token_test.go
  hash/
    hash.go
  internal/
    trace/
      trace.go

go.mod

module github.com/someuser/modname

と記述されているとすると、ルートディレクトリの.goファイルでpackage modnameと宣言することで、ユーザーはこのパッケージを

import "github.com/someuser/modname"

でインポートできます。また、他のパッケージは

import "github.com/someuser/modname/auth"
import "github.com/someuser/modname/auth/token"
import "github.com/someuser/modname/hash"

でインポートできます。
internal/trace内のtraceパッケージは、外部のパッケージからはimportできません。外部に公開する必要のないパッケージは、可能な限りinternalに格納するべきです。

複数のコマンドラインプログラムを含むモジュール

同じリポジトリに複数のコマンドラインプログラムがある場合、各プログラムは別々のディレクトリに置かれることが多いです。

project-root-directory/
  go.mod
  internal/
    ... ユーティリティパッケージ
  prog1/
    main.go
  prog2/
    main.go

各ディレクトリの、プログラムを記述した.goファイルではpackage mainと宣言します。internalディレクトリには、コマンドラインプログラム間で共有されるユーティリティパッケージを置きます。
ユーザーは、

$ go install github.com/someuser/modname/prog1@latest
$ go install github.com/someuser/modname/prog2@latest

とすることで、それぞれのプログラムを使用できます。
複数のコマンドラインプログラムがある場合、各プログラムのディレクトリをすべてcmdディレクトリに格納してしまう慣習もあります。コマンドラインプログラムしか含まないリポジトリでは必ずしもこうする必要はありませんが、パッケージとコマンドラインプログラムが混在する次のようなケースでは便利です。

パッケージとコマンドラインプログラムが混在するモジュール

同じリポジトリに、パッケージとコマンドラインプログラムが混在することもあります。例えば、以下のような構成が考えられます。

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
  internal/
    ... internal packages
  cmd/
    prog1/
      main.go
    prog2/
      main.go

このモジュールがgithub.com/someuser/modnameという名前だったとすると、ユーザーは以下のようにパッケージをインポートできます。

import "github.com/someuser/modname"
import "github.com/someuser/modname/auth"

また、以下でプログラムをインストールできます。

$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest

サーバー

多くのプロジェクトが Go でサーバーを実装しています。サーバーには、プロトコル(REST・gRPCなど)・デプロイ方法・フロントエンド・コンテナ・その他のスクリプトなどによって、さまざまな構成がありえます。ここでは、サーバープロジェクトのうち Go で実装された部分に絞って説明します。

サーバーはそれ自体で完結したプログラムで、パッケージを export しない場合が多いです。その場合、Go サーバーのパッケージはinternalディレクトリに置くことを推奨します。また、多くの場合は Go 以外のコードを含むディレクトリが別にあるので、Go サーバーのプログラムはcmdディレクトリに格納するとよいです。

project-root-directory/
  go.mod
  internal/
    auth/
      ...
    metrics/
      ...
    model/
      ...
  cmd/
    api-server/
      main.go
    metrics-analyzer/
      main.go
    ...
  ... その他、Go 以外のコードを格納するディレクトリ

プロジェクトが大規模になって、パッケージが他プロジェクトでも使いまわせるほどに成長したら、それらを別のモジュールに切り出すのがいいでしょう。

Discussion