2022/05/08

[golang]go.modの入れ子を理解できなかった

go.mod をずいぶん理解できたんじゃないかと思っていたのだが、入れ子になった場合、つまり サブディレクトリの中にも go.mod がある場合にどうしたらよいのかがわからなかった。

 

なんで入れ子にしようとしたかというと、gRPC 用の proto ファイルを別管理しようとしていたからだ。過去の記事にも同じことを書いたような気がするが、

  • proto ファイルをサーバ側のアプリと同じリポジトリに置いた
  • でも proto ファイルの方はバージョンが変わらずサーバアプリだけ更新されることが多い
  • うーん、ならば go.mod を別にした方がよいのでは?

という思考からだ。

リポジトリごと分ける方が無難なのだろうけど、それはそれでやりすぎな気がしてそうしたのだ。
サーバもクライアントもアプリが小さいなら同じリポジトリに置いてしまってよかったかもしれないが、clone するだけで大ごとになるようなサイズなので「サーバ+proto」「クライアント」の2つに分けている。

と「仕方ない」という感を出そうと思ったが、初回のリリースまでは proto もサーバアプリと同じ go.mod にして、リリースしたらどうせ tag を付けるだろうからバージョン管理で逃げてしまえばよかったと思う。
開発中は tag を付けていないので、go.mod に書くときは commit-id を元にした pseudo-version を使っている。

 

入れ子にしたのはこれだけではなかった。
クライアント側も gRPC にアクセスする部分だけ go.mod を別にしてしまったのだ。
gRPC のテスト用クライアントアプリを別に作ったので、gRPC アクセス部分だけ使い回そうとしたのだ。

そんなこんなして、面倒な構成になってしまった。
実際はこんなにシンプルではなく、もっと大量に import している。

image

こうなったとき、go.mod をどう書いて良いのかが結局分からなかった。
go mod tidy してもエラーが出て、いろいろ書き換えても解消できなくなったのだ。 replace を使ってローカルディレクトリを参照したのも原因かもしれない。

結局あきらめて、クライアントアプリの grpcアクセス部分は go.mod を削除して単にクライアントアプリの 1パッケージとして使うことにした。

image


悔しいからいうわけではないが、この構成に変更してよかったと思う。
そもそも、テストアプリはテストアプリに過ぎないのだから、そのために苦労しすぎるのは馬鹿らしい。
それに、grpc の部分を本格的に別のところでも使いたいならクライアントアプリとは別リポジトリにすべきだろう。サーバが複数あることは考えにくいからセットにしたが、クライアントは種類が複数あっても変ではないのだ。

 

だから構成を変更したことに対しては文句がないのだが、その理由が「できなかった」というのは納得がいかないものだ。
できるけどやらない、くらいにしておきたい。

というわけで、入れ子になった go.mod 環境を作って試していこう(本題)。


その前に、用語の整理をしたい。
パッケージは goファイルの中に package として書くのでわかるのだが、モジュールとかバージョンになるとちゃんと理解できていない。今回だって「go.mod の入れ子」と書いているが、正式な名称じゃないと思う。

Modules, packages, and versions
https://go.dev/ref/mod#modules-overview

モジュール

これは package なんかの集合で、識別はモジュールパスで行われる。
モジュールパスは go.mod に依存関係などと一緒に書く。
モジュールルートディレクトリは go.mod があるディレクトリ。
メインモジュールはちょっと違って、go コマンドを実行したディレクトリに含まれているモジュールを指す。つまりルートディレクトリの下にあるサブディレクトリで go コマンドを実行しても、そこに go.mod が含まれていなければ上にたどって直近にある go.mod のモジュールがメインモジュールになるということだ。

git で管理していて普通に作るなら、モジュールパスは git のリポジトリと同じにするだろう。 git 以外でも同じようなものだろう。
ローカルファイルでも悪くはないと思うのだが、どうなんだろうね。


というわけで、ローカルディレクトリで試していこう。

$ go version
go version go1.18.1 linux/amd64

最近は GOPATH を使わなくてもよくなりつつあるようだが、よくわからんので GOPATH を使う。

$ export GOPATH=`pwd`
$ mkdir bin src
$ cd src
$ mkdir mod1 mod2
$ cd mod1
$ go mod init
$ cd ../mod2
$ go mod init
$ cd ..

これで mod1 と mod2 というモジュールができた。
モジュールパスも mod1 と mod2。

なにか import させて go.mod を賑やかにしたいので、昔作った gogo-test1 を持ってこよう。

$ cd mod1
$ go get github.com/hirokuma/gogo-test1
go: downloading github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
go: github.com/hirokuma/gogo-test1@v0.0.0-20211121014212-382e4677bbfb requires
        github.com/hirokuma/yoshio@v0.0.0: reading github.com/hirokuma/yoshio/go.mod at revision v0.0.0: git ls-remote -q origin in /home/xxxx/golang/pkg/mod/cache/vcs/301146d5f0494aad6678724d644f7711fa7ff362d0bc8683a33b7d297ca8e2c3: exit status 128:
        remote: Repository not found.
        fatal: repository 'https://github.com/hirokuma/yoshio/' not found

なんだこりゃ?と思ったが、 gogo-test1 の go.mod で require github.com/hirokuma/yoshio にしてて、それを replace で github.com/hirokuma/gogo-test2 にしているだけだ。

gogo-test1 を clone して go mod tidy してもエラーにならない。
もしかして replace って import すると使えなくなったりするんだろうか。

依存関係がない gogo-test4 はすんなり go get できた。

$ go get github.com/hirokuma/gogo-test4

go.mod

module mod1

go 1.18

require github.com/hirokuma/gogo-test4 v0.1.0

 

main.go

package main

import (
    "fmt"

    gogo "github.com/hirokuma/gogo-test4"
)

func main() {
    gogo.SetValue(3)
    fmt.Printf("%v\n", gogo.GetValue())
}

$ go run .
3


hirokuma/gogo-test1 がダメな理由だが、なんとなく tag で付けた v3 もよくない感じがする。
gogo-test1/go.mod にあった replace をこちらにも追加したのだが、これはダメだった。

$ go mod tidy
go: errors parsing go.mod:
/home/ueno/golang/src/mod1/go.mod:7: no matching versions for query "v3"

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v3

 

こっちだと go mod tidy できた。

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb

 

tag に v0.0.3 という名前で追加しても成功したので、 x.y.z 系の tag を付けておくのが無難ということか。
本題とは関係ないのに時間がかかってしまった。


というところで気付いたが、ローカルディレクトリだとバージョンも何もないので意味が無いのでは・・・?

とりあえずやっておこう。

src/
  mod1/
    go.mod
    main.go
  mod2/
    go.mod
    gogo.go

 

mod2/go.mod

module mod2

go 1.18

mod2/gogo.go

package mod2

import "fmt"

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
}

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
}

$ cd mod1
$ go run .
gogo-test1!
gogo!
mod2 GoGo!

mod2 は replace するから require しなくてもよいかと思ったが、それはダメだった。


ここで、mod2 の下に modmod2 を追加する。

image

 

mod2/modmod2/gogogo.go

package modmod2

import "fmt"

func Gogogo() {
    fmt.Printf("modmod2 GoGo!\n")
}

そして mod1 で使う。

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2
replace modmod2 => ../mod2/modmod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0
require modmod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"
    "modmod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
    modmod2.Gogogo()
}

$ go run .
gogo-test1!
gogo!
mod2 GoGo!
modmod2 GoGo!

 

なんというか、これだと単にディレクトリの位置が違うというだけで、入れ子になっているのは関係ないことになる。

バージョンのことを除けば、go.mod が入れ子になろうと関係がないということかしら。
いつも親?のモジュール名のサブディレクトリっぽいモジュール名を付けていた(今回なら mod2/modmod2 みたいな)ので go.mod がないときと同じように扱っていたのだが、全然違うモジュール名を付けていてもなんとかなったのかもしれない(どうやってリポジトリと紐付けるのかは知らんが)。

mod2/gogo.go から modmod2.Gogogo() を呼ぶこともできた。

package mod2

import (
    "fmt"
    "modmod2"
)

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
    modmod2.Gogogo()
}

こちらは mod2/go.mod に追加せずにできた。

mod1/go.mod のように追加しなければならないかと思ったのだが、そこはサブディレクトリという特権なのか?
ディレクトリ名を mod2/modmod2 から mod2/modmodmod2 に変更だけしてみたら、mod1/go.mod の replace したパスが存在しないというエラーになった。

$ go run .
../mod2/gogo.go:5:2: modmod2@v0.0.0: replacement directory ../mod2/modmod2 does not exist

mod1/go.mod の replace を ../mod2/modmodmod2 に変更すると go run できた。
なんで mod2/gogo.go は変更しなくてよかったのだろう?
キャッシュかもしれんと考えて各 go.mod があるディレクトリで go clean -modcache と go clean -cache を実行したが変わらなかった。

まあ、やっぱりサブディレクトリの特権ということか。


そう考えると、あまりサブディレクトリに go.mod を持つ利点よりも、バージョン管理が難しくなる欠点が目立ちそうなので避けたい気もする。

最初に書いた proto のディレクトリに go.mod を付けたとしても、tag をディレクトリごとに付けられるわけでもないので、やるなら v0.0.1-proto みたいにサフィックスを付けるとかになるんだろうか。

バージョンの付け方は go.dev に説明がありそうだったが・・・読む気にならん。

Minimal version selection (MVS)
https://go.dev/ref/mod#minimal-version-selection

 

golang の言語仕様自体は結構シンプルな印象を受けているのだが、 go.mod のことで悩む時間が多いのがきついところだ。なんとなく、バージョン管理にまで口を出す傲慢な言語、っていうイメージになってしまった。まあ Java が出てきたときも「ディレクトリが言語に合わせないといけないなんて!」と思ったので今さらかもしれん。オープンソースとはいえ開発主体があるならその意向に沿ってしまうのは仕方ないだろう。嫌なら自分で開発すれば良いだけなのだから。

 

最後の方になって役に立ちそうな記事を見つけた。

Go のモジュール管理【バージョン 1.17 改訂版】
https://zenn.dev/spiegel/articles/20210223-go-module-aware-mode

ありがたや。