Goの共通モジュール管理の方法をまとめてみた(Go1.18で導入されたWorkspaceも)


概要

Amplifyを使ってサービスを構成する方針で、バックエンドにLambda関数をいくつか用意する必要がありました。
その際に、Amplify-cliを使って構築してみたのですが、

  • GoでLambda Layerを生成するコマンドがなかった
  • Lambda関数のみで生成するとgo.modが分かれて、lambda関数単位のプロジェクトが生成されてしまった

これらのことからGoで共通モジュールをいい感じに管理する方法ないかなと探っていたところ、Go1.18で導入されたWorkspaceが機能的にマッチしていたのでまとめてみました。
そのついでに色々まとめてみた記事です。

手っ取り早くWorkspaceのやリ方だけをみたい方はこちらだけ参照してください

Goで共通モジュールを取り扱う方法

go1.18以前にGoで共通モジュールを使う方法は2つ(知ってる限り)

  • Go1.8(1.18ではない)から導入されたPlugin
  • Gitでversion管理されたもの
    • この補助として1.18で導入されたWorkspaceは機能する

Plugin

ディレクトリは最終的にこんな感じになります
これに従って作成を進めます

multimodule
├── go.mod
├── main.go
└── pluginmodules
    ├── go.mod
    ├── plugin.go
    └── plugin.so

ディレクトリとgo mod生成

~/ $ mkdir multimodule
~/ $ cd multimodule
~/multimodule/ $ go mod init multimodule
~/multimodule/ $ mkdir pluginmodules
~/multimodule/ $ cd pluginmodules
~/multimodule/pluginmodules/ $ go mod init pluginmodules

~/multimodule/pluginmodules/plugin.goファイルを用意します

package main

import (
	"fmt"
)

func Run(from string) {
	fmt.Printf("これはプラグインです。%sから呼ばれました", from)
}

~/multimodule/pluginmodules/plugin.goファイルをビルドします
すると、plugin.soファイルが生成されます

~/multimodule/pluginmodules $ go build -buildmode=plugin -o plugin.so

ビルドしたPluginファイルを~/mutimodule/main.goの方で使用します

package main

import (
	"fmt"
	"os"
	"plugin"
)

func main() {
	var (
		pl      *plugin.Plugin
		err     error
		runFunc plugin.Symbol
	)

	if pl, err = plugin.Open("./pluginmodules/plugin.so"); err != nil {
		fmt.Println("プラグインの読み込みに失敗しました")
		os.Exit(1)
	}
	if runFunc, err = pl.Lookup("Run"); err != nil {
		fmt.Printf("プラグインの実行に失敗しました: %s", err)
		os.Exit(1)
	}

	runFunc.(func(string))("Mainファイル")
}

実行

~/multimodule/ $ go run main.go
# 実行結果
これはプラグインです。Mainファイルから呼ばれました

Pluginを使う場面

これで共通モジュールを作成できました。
使う場合は、さまざまなプロジェクトからplugin.Open("./modules/plugin.so")でOpenしましょう
個人的に共通モジュールの管理でPluginを使うくらいならGitで管理する方法を選ぶと思いますが、CLIからGoのファイルを指定して実行させたい場合に有効でしょうか、というかそういうのを想定しているからPluginって名前なんでしょうね

例えば、テスト用のデータを生成したい場合に、用意しておいたGoのSeederファイルをCLIで指定してデータを流し込みたい場合

  1. xxx_seeder.goファイルを作ってRunメソッドのみ定義する
  2. soファイルにBuildしておく
  3. seedrunnner.goのようなものを用意して、flagパッケージを使って-fileオプションを受け付けるようにする
  4. seedrunner.go -file xxx_seederという名前を渡す
  5. plugin.Open(fmt.Sprintf("./seeder/%s.so", filename))で開く
  6. runFunc, err := pl.Lookup("Run")でRunメソッドを読み込む
  7. runFunc.(func())()でSeederを実行する

って感じでしょうか。
Seederの事前ビルドが必要なので、Makefileなどで一手間加えておくと良きだと思います。

Gitで管理する方法

Goはgitで管理されているモジュールをgo getで取得することが可能です
この方法がメジャーだと思います。

ディレクトリは最終的にこんな感じになります
これに従って作成を進めます

test
├── gitmodules
│   ├── go.mod
│   └── plugin.go
├── go.mod
└── main.go

まずはgithubでrepositoryを作成(これは省きます)
次にディレクトリとgo mod生成

~/ $ mkdir multimodule
~/ $ cd multimodule
~/multimodule/ $ go mod init multimodule
~/multimodule/ $ mkdir gitmodules
~/multimodule/ $ cd gitmodules
~/multimodule/gitmodules/ $ go mod init github.com/y-saiki1/gitmodules # 自分で作ったgithubのリポジトリ名にしてください

次に共通モジュールとして~/multimodule/gitmodules/plugin.go作成

package gitmodules

import "fmt"

func Run(from string) {
	fmt.Println("これはgit管理されたモジュールです。%sから呼ばれました", from)
}

用意したファイルをリポジトリにpush

~/multimodule/gitmodules/ $ git init
~/multimodule/gitmodules/ $ git remote add origin [email protected]:y-saiki1/gitmodules.git # 自分で作ったリポジトリ名にしてください
~/multimodule/gitmodules/ $ git add .
~/multimodule/gitmodules/ $ git commit -m 'first commit'
~/multimodule/gitmodules/ $ git push origin master

githubにpush後、~/multimoduleディレクトリで以下を実行して、pushしたGoプロジェクトをgo modに追加する

~/multimodule/ $ go get github.com/y-saiki1/gitmodules # 自分で生成したGithubリポジトリを参照してください

go getが終わったら、共通モジュールを使用するため~/multimodule/main.goファイルで共通モジュールを使用する

package main

import (
	// 以下追加(ここは自分のリポジトリに読み替えてください)
	"github.com/y-saiki1/gitmodules"
)

func main() {
	gitmodules.Run("Mainファイル")
}

実行

~/multimodule/ $ go run main.go
# 実行結果
これはgit管理されたモジュールです。Mainファイルから呼ばれました

PRIVATEリポジトリを使っている方は事前にgithub上にトークンを設定してからやらないと共通モジュールをgo getできないので注意です。
こちらの方の記事が参考になります