🦜

Goのローカルパッケージのインポートについて

2023/03/31に公開

あちこちで「サブパッケージ参照するならマルチモジュール化すると良い」という誤解記事が増え続けているので、コメントがかける記事であればこの記事でお伝えしたい内容を簡単にコメントするようにしていますが、コメント機能の無い記事もあり、カウンターとしてこの記事を書いておきます。

ただの一手法として紹介しているならいいんですが、それらの記事を見て「Goではローカルパッケージ参照にこんな面倒なことしなきゃいけないのか!」という誤解を重ねた反応も観測されています。

しかし「Goでローカルパッケージのインポート」でググると半数弱の記事が誤解している記述または誤解しやすい記述なので困っています。 ー> 2024年時点でずいぶん改善しました!よかったー。

問題の手法

以下のような構成でmain.goからsub1パッケージを参照する方法を模索。

- project1/
  - sub1/
    - sub1.go
  - main.go
  - go.mod

ローカルのパッケージ参照が上手くいかない・・・相対だとエラー。
ググると半数ほどが冗長な手法を紹介している(特に日本語の記事)
その誤解した記事を見て以下のようにしてしまう。

- project1/
  - sub1/
    - sub1.go
    - go.mod
  - main.go
  - go.mod
sub1/go.mod
module project1/sub1
project1/go.mod
module project1

replace project1/sub1 => ./sub1

これでgo mod tidyが通ってしまいます。

が、単純にローカルにあるサブパッケージを参照したいためだけに
マルチモジュール化+go.modにreplace手法を使うのは冗長すぎます。

もちろん、別の公開モジュールを一時的にローカルで参照したいのであればOKな手法でそのように解説しているものは問題ありませんが、初心者がそれらの記事をローカルインポートの手法であると誤解して実践しようとしてしまっているのが厄介なのです。

パッケージとは

  • Goのコードからimport可能とするための概念
  • *.goファイルを含むフォルダーを「パッケージ」として扱います
  • 同じフォルダ内のGoファイルの先頭にはpackage パッケージ名が統一されていることが必要です
  • パッケージ名の命名が必要でフォルダ名と同じにすることが推奨されています
  • go.modファイルが存在してしまうと単純パッケージではなくモジュールの扱いになっちゃうのに注意

モジュールとは

  • VCSで版管理するために設けられた概念
  • 単一のパッケージまたは複数サブパッケージをまとめたもの
  • モジュールの命名が必要
  • バージョンにはセマンティックバージョンを付与
  • モジュール直下パッケージの参照には「import "モジュール名"」を使う
  • サブパッケージの参照には「import "モジュール名/サブパッケージパス"」を使う

Goの開発モード

GOPATHモードについて

  • Go1.10以前ではこのモードしか使えない
  • 相対パスインポートが使えた
  • {GOPATH}/src/配下にプロジェクトも依存ライブラリも置く必要があった
  • 依存は実際にGOPATH配下に置かれたソースによって解決するので版の制御はVCSの操作依存だった
  • したがって、実際のプロダクトの版管理ではgit-submoduleなどを活用する必要があった
  • もう使うべきではない

モジュールモードについて

  • 依存するモジュールと公開リポジトリは極力一対一に関連付けられることを推奨
  • 相対パスインポートが使えなくなった
  • go.modファイルのあるフォルダがモジュールルート(go mod initで作成できる)
  • GOPATH配下はbinと依存キャッシュ置き場という役割になり、
  • 開発中のコードを{GOPATH}/src/配下に置くことは必須ではなくなった
  • 依存解決には版も考慮されるようになりVCS操作も自動的かつ適切に行われる
  • これからのプロジェクトにはこちらのモードを使うべき

ローカルパッケージ参照について

ここを参考にしましょう。

https://zenn.dev/nobonobo/articles/4fb018a24f9ee9

マルチモジュールやreplaceなどしなくてもシンプルにローカルのサブパッケージを参照できます。

replaceの使いどころ

a. 公開(もしくはプライベートな)モジュールを一時的にローカルで参照したい
b. 参照モジュールの別バージョンを参照したい
c. 参照モジュールのフォーク(別のリポジトリ)を参照したい

というあたりで初めてgo.modにreplaceを書きます。ただ、replaceは臨時のためのもので恒久的なプロダクトに使われているのは望ましくありません。

将来的には極力replace記述はなくなるようにしましょう。

a.なら開発が安定したら、b.なら後方互換を確認出来たら、c.なら関連の修正が本家に取り込まれたら->公開モジュールの直接参照に修正しましょう。

プライベートリポジトリの参照

マルチモジュール化したい理由の一つにプライベートリポジトリ参照がしにくいのもあると思いますが、方法はちゃんとあります。

https://go.dev/doc/faq#git_https

マルチモジュールとworkspaceモード

  • プロダクトが大きくなってきて単一モジュールでの版管理が難しくなり、
  • 単一のリポジトリで部品ごとに独立してバージョン更新していくためにマルチモジュールを使う
  • go workspaceモードが追加されたのでマルチモジュールを扱うならこのモードを使うと良い
  • workspaceモードではもうgo.modにreplaceを使う必要はなくなってる

workspaceモードに関する情報はここを参考に。
https://go.dev/doc/tutorial/workspaces

まとめ

  • 「相対パス参照」や「GOPATHモード」はもう使わないようにしよう。
  • ほとんどのケースでは単一のメインモジュールだけで十分です。
  • 明らかにモジュールにしたいわけじゃないサブパッケージをモジュールにする必要はありません。
  • サブパッケージフォルダからはgo.modファイルを消しましょう!
  • go.modにreplaceを使う状況はほぼなくなりました。
  • ローカルパッケージ参照は正しい方法であればシンプルです。
  • プライベートリポジトリを参照する方法はあります(GitHub等)。
  • マルチモジュールとするときはgo-workspaceモードを使いましょう。
  • ググると半数くらい間違ったreplaceの使い方が見つかるので注意しましょう。
  • ChatGPTにさらっと聞いたら間違いばっかり。

おまけ

ChatGPTに以下のように2段階で聞いたら正しい答えが返ってきた。

  • 「Goメインモジュールの作成方法を教えて。」
  • 「そこにサブパッケージを追加して参照するには?」

Discussion