Goのプロジェクト構成の基本

3 min read読了の目安(約3500字

Goのプロジェクトをどの様なファイル構成で配置すれば良いか読み物が少ないという指摘を見たのでまとめてみようと思う。

https://twitter.com/Flast_RO/status/1365068082939486208

GOPATHについて

Go1.16がリリースされたことでGo-Moduleによるプロジェクト構成が標準で推奨されることになりました。(Go1.11までさかのぼってGo-Moduleは使える様になってます)

Go-Moduleモードでは「GOPATH配下にプロジェクトを置かなければならない」という制約からは解放されています。なので、実質GOPATHはどこを指していても構わないし設定されていなくても「ユーザーホーム/go」というデフォルトの場所が決まっているので開発できます。

おすすめの環境変数設定は以下の2つだけ。

  • 「GOPATH=~/.go」(WindowsはGOPATH=%USERPROFILE%\.go)
  • 「PATH=$GOPATH/bin:$PATH」(WindowsはPATH=%GOPATH%\bin;%PATH%)

初学者はその他のGOXXXX環境変数をいじらないことをお勧めします。goenvやgvmなどのマルチバージョン管理も使わない方が良いと思います。(GOPATHの場所が複数になると開発体験が低下します)

モジュールとパッケージとは

  • モジュール=パッケージを一つまたは複数のサブパッケージを取りまとめたカタマリ。
  • パッケージ=フォルダ単位で単一ファイルまたは複数ファイルのカタマリ。
  • サブパッケージ=サブフォルダにおくだけで扱いはパッケージと同等。

GOPATHモードの時にはモジュールという概念はなかったんだけど、パッケージとそのサブパッケージをまとめて「モジュール」として扱う概念が追加されたということ。

あるフォルダにてモジュールだと宣言するとそのフォルダ以下がモジュールになります。
宣言方法は以下の通り

> go mod init <モジュール名>

<モジュール名>github.com/nobonobo/moduleAというような表記になります。
(モジュールを公開しない場合はmoduleAという名前でも良い)

こうするとカレントフォルダにgo.modファイルが作成され、このカレントフォルダ以下がまるっと「モジュール」の扱いになります。

パッケージ名とはパッケージに置かれるGoコードの先頭に宣言される名称で、これは極力そのパッケージのフォルダ名と合致させることが推奨されます。モジュール名も、最後の部分パス名(前述の例であればmoduleA)とモジュールのフォルダ名は合致している方が良いと思います。

ファイル構成例:GitHub公開版

  • project1/ :プロジェクトルート=モジュールルート=リポジトリルート
    • subpkg1/
      • sub1.go
    • subpkg2/
      • sub2.go
    • app.go
    • go.mod
go.mod
module github.com/nobonobo/project1

go 1.16
sub1.goの冒頭
package subpkg1
sub2.goの冒頭
package subpkg2

以上のような構成の場合、
subpkg1やsubpkg2は"<モジュール名>/subpkg1""<モジュール名>/subpkg2"という記述でインポートできます。

app.goの冒頭
package main

import (
	"github.com/nobonobo/project1/subpkg1"
	"github.com/nobonobo/project1/subpkg2"
)

ファイル構成例:非公開版

ルールは何も変わらない。

  • project1/ :プロジェクトルート=モジュールルート=リポジトリルート
    • subpkg1/
      • sub1.go
    • subpkg2/
      • sub2.go
    • app.go
    • go.mod
go.mod
module project1

go 1.16
sub1.goの冒頭
package subpkg1
sub2.goの冒頭
package subpkg2
app.goの冒頭
package main

import (
	"project1/subpkg1"
	"project1/subpkg2"
)

複雑な事例

モジュール配下にサブモジュールを置いた場合などもっと複雑な事例はあるけれど、あまり有効なものとは思えないです。基本は「リポジトリ1つ」に「モジュール1つ」です。

相対パスインポートについて

以下の様に相対パスによるインポートはGOPATHモードで利用できていました。

import "./subpkg1"

もともと非推奨だったのですが、Go-Moduleモードでは禁止されエラーになる様になりました。
なので、「相対パスインポート」を見たら「古いコードだな」と思ってあまり参考にしない様にしてください。

マルチバージョン標準の方法

GOPATHは以前はプロジェクトソース置き場としての役割もありましたが、Go-ModuleモードでのGOPATHは依存コードとコンパイル済みキャッシュ、バイナリ置き場としての役割に徹します。

> go get golang.org/dl/go1.15.8
go: downloading golang.org/dl v0.0.0-20210220033039-562909534da3
go: found golang.org/dl/go1.15.8 in golang.org/dl v0.0.0-20210220033039-562909534da3
> go1.15.8 download
Downloaded   0.0% (    15247 / 122353388 bytes) ...
Downloaded   6.9% (  8421376 / 122353388 bytes) ...
Downloaded  27.4% ( 33570816 / 122353388 bytes) ...
Downloaded  53.1% ( 65011712 / 122353388 bytes) ...
Downloaded  68.6% ( 83902464 / 122353388 bytes) ...
Downloaded 100.0% (122353388 / 122353388 bytes)
Unpacking /Users/nobo/sdk/go1.15.8/go1.15.8.darwin-amd64.tar.gz ...
Success. You may now run 'go1.15.8'

以上の操作でgo1.15.8というコマンドでそのバージョンのGoを利用できる様になります。

このとき、どのGoのバージョンを使う場合でもGOPATHの場所を変えていなければ、依存コードキャッシュの効果は最大限発揮されます。gvmやgoenv等はGoバージョンごとにGOPATHを切り替えてしまうので依存はダウンロードし直しになってしまいますし、エディタのLanguageServer(コード補完などの支援)にGOPATHの変更を再設定しなければなりません。

まとめ

  • 結構古い読み物を参考にGOPATH配下にプロジェクトを置かなければという誤解が後を絶たない。
  • Go-Moduleモードを基本に使っていこう。
  • 初心者は環境変数いじらなくてもいいと思う。
  • Go-ModuleモードではGOPATHは依存キャッシュとして使われるだけになった。
  • gvmやgoenvなどは使わない方がいいよ(DX悪化しちゃう。特にエディタ連携が辛くなる)
  • 相対パスインポートを見たらそのコードは古い可能性が高いので参考にしない様に。
  • 標準のGoのマルチバージョン利用方法があるよ!