新参者なので Go言語 を使ってモジュールを切り出す手法として $GOPATH を設定する方法と Go Modules(go mod コマンド) を使用する方法とで混乱してしまったのですが Go Modules の方が新しく2019年度の公式サイトのモジュール解説ページでも使用されており、これからの主流になりそうなのでこちらを使うことに。
公式チュートリアルだけではあまり理解できてる気がしなかったので記事にしました。
公式チュートリアルもおすすめですが自分でディレクトリを切って作ったモジュールを import したい人がすぐに把握できるようにまとめています。
参考:Understanding go.mod and go.sum - https://faun.pub/understanding-go-mod-and-go-sum-5fd7ec9bcc34
参考:Go Modules Reference - https://go.dev/ref/mod#go-mod-file
go.mod とは何か ~前提の知識~
現ディレクトリの goファイル上 におけるモジュールの依存関係のルートとなるファイルのことです。モジュールの依存関係のルートとなることで goファイル をモジュール化します。
go mod init のコマンドを実行することで goファイル上 で import されたすべてのモジュール(やライブラリ)のエントリーを作成した go.mod ファイルを生成できます。
go get コマンドで一つ一つのモジュールやその依存関係を引っ張る手間を省くことができます。
go mod init で go.mod ファイルを生成します。 go mod init は go.mod ファイルを初期化した上で現在のディレクトリからモジュールを作成してくれるコマンドです。
go mod は go v1.11~ から対応。
参考:公式サイト - https://go.dev/blog/using-go-modules
go.sumとは
外部のモジュールをimportしていると go mod init or go mod tidy を実行して生成されるハッシュだらけの go.sum ファイルですが以下のためにあります。
直接・間接を問わず依存先モジュールのハッシュを記録するためのファイルです。go.modと共にリポジトリにpushされビルド再現性のために利用されますが、モジュールの取得はgo.modのrequireディレクティブにある情報で完結できるためgo.sum自体は無くとも原則としてビルド再現性は得られます。
ではなんのためにgo.sumがあるのかというと、go.modを元に取得したモジュールが本当にgo.sum生成時のものと一致しているかのチェックのためです。バージョンはgitのタグを元に管理されますがその気になれば付け替えること自体は可能です。
引用:go.modとgo.sumの読み方(メルペイの人の記事) - https://zenn.dev/ryo_yamaoka/articles/595cf9e69229f9
本題:go.modで自分の作ったモジュールをimportできるようにする
ここからが本題。
ディレクトリ
module_practice
greetings/
greetings.go
go.mod
hello/
hello.go
go.mod
main.go
go.mod
完成系↓
基本系
まずはmain.goから
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
go.modファイルのない状態で go run . を実行しても実行側で main.go のディレクトリを取得できないため go.mod file not found in current directory のエラーが生じます。
したがって go mod init コマンド
go mod init {ドメイン名}/{任意のモジュール名}
特になければ上記で modファイル を生成することが多いようです。
$ go mod init local.package/main
module local.package/greetings
go 1.18
今回は上記のモジュール名で生成します。local.package/main の main を hoge に変えてもモジュール化できました。{ドメイン名}もなくてもモジュール化はできます
また、goファイル上で定義した package名 と {任意のモジュール名} は同一でなければならないと勝手に勘違いしていました。といってもわかりやすいので同一にした方が無難かなと。
$ go run .
Hello World!
で動作確認できました。
greetings.goをモジュール化してmain.goへimportする
$ cd greetings
$ go mod init local.package/greetings
package greetings
import (
"errors"
"fmt"
)
func Hello(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
module local.package/greetings
go 1.18
main.goの編集
package main
import (
"fmt"
"log"
"local.package/greetings"
)
func main() {
message, err := greetings.Hello("呂布カルマ")
if err != nil {
log.Fatal(err)
}
fmt.Println(message)
}
"local.package/greetings" は先ほどモジュール化した main.go 側の go.mod でローカルパスを渡してあげないといけません。このまま使うと
main.go:7:2: no required module provides package local.package/greetings; to add it:
go get local.package/greetings
といった内容で go get しろと怒られますが、go getはすでにgithubなどでアップロードされているモジュールをDLするコマンドなので
module local.package/main
go 1.18
replace local.package/greetings => ./greetings // ローカルパスへ置き換えるためモジュール名から相対パスを replace で渡す。
$ go mod tidy
を実行すると
module example.com/main
go 1.18
replace local.package/greetings => ./greetings
require local.package/greetings v0.0.0-00010101000000-000000000000 //自動でバージョンと一緒に require してくれる
となり、この状態で
$ go run .
Hi, 呂布カルマ. Welcome!
を確認できたかと思います。
以上の要領でモジュールを引っ張ってこれます。go.mod を生成し、replace で相対パスを渡してあげて go mod tidy で require を取り込むことでモジュールを引っ張ってこれます。
hello.goをさらに噛ませる
先ほどまでは greetings.go -> main.go でしたが
greetings.go -> hello.go -> main.go というように hello.go に greetings.go を依存させた形で import したいと思います。
package hello
import (
"log"
"local.package/greetings"
)
func Hello(msg string) string {
log.SetPrefix("greetings: ")
log.SetFlags(0)
message, err := greetings.Hello(msg)
if err != nil {
log.Fatal(err)
}
return message
}
$ cd hello
$ go mod init local.package/hello
$ go mod edit -replace local.package/greetings=../greetings
// ↑replace local.package/greetings => ../greetings を go.mod へ書き込むコマンド。このようにもできます。
$ go mod tidy
をやって、これでgreetingsを引っ張ってこれました。
module local.package/hello
go 1.18
replace local.package/greetings => ../greetings
require local.package/greetings v0.0.0-00010101000000-000000000000
main.goでhello.goを引っ張る
下のように編集
package main
import (
"fmt"
"local.package/hello"
)
func main() {
message := hello.Hello("ポカホンタス")
fmt.Println(message)
}
module local.package/main
go 1.18
replace local.package/hello => ./hello
これで
$ go mod tidy
をすると
go mod tidy
go: found local.package/hello in local.package/hello v0.0.0-00010101000000-000000000000
go: downloading local.package/greetings v0.0.0-00010101000000-000000000000
local.package/main imports
local.package/hello imports
local.package/greetings: unrecognized import path "local.package/greetings": https fetch: Get "https://local.package/greetings?go-get=1": dial tcp: lookup local.package: no such host
のように怒られてしまいます。しかし main.go側 で使っているパッケージは確かに local.pakcage/hello です。go.mod の replace にも書いてあります。
これは、Go が Minimal version selection (MVS) というモジュールのバージョンを選択しながらモジュールを引っ張ってくる探索手法をとっているからだそうです。
ソース:https://go.dev/ref/mod#minimal-version-selection
詳しく理解できてないので詳細は割愛しますが、依存先の依存先(つまりgreetings.go)のバージョンも取得した上で hello.go を呼び出さないといけません。
go moduleのreplaceでハマったこと - Zenn(森に帰省中のゴリラ) がMVSについてもう少し深堀しています。
今回はローカル上でバージョンの取得も行うため replace を使っています。
すなわち、
module local.package/main
go 1.18
replace local.package/greetings => ./greetings // greetingsも置換対象としてローカルでバージョン指定する
replace local.package/hello => ./hello
$ go mod tidy
// ~ 省略 ~
require local.package/hello v0.0.0-00010101000000-000000000000
require local.package/greetings v0.0.0-00010101000000-000000000000 // indirect
indirectとして依存先の依存先である greetings をバージョン指定で呼び出せました。
$ go run .
Hi, ポカホンタス. Welcome!
~ fin ~