CLOVER🍀

That was when it all began.

非VCS環境下で、Goでローカルにあるモジュールを扱う

これは、なにをしたくて書いたもの?

Goを使ったプログラムを書いていこうと思うのですが、モジュールに関する情報を全然知らないので、試しながら学んで
みようと。

プログラムを書いていたら、パッケージくらいは使うでしょう、と。

なお、ここで試す条件は、以下の通りです。

要するに、ローカルでとりあえず作って動かしたいというケースを想定しています。

作成するソースコード$GOPATH配下にまとめていると、もしかして今回のreplaceを使う話って発生しないんですかね…。
※現時点で$GOPATHは見ないまま書いています

モジュールに関する情報を見る

なにはともあれ、ドキュメントです。

Modules · golang/go Wiki · GitHub

リファレンスもあります。ただ、開発中だとは書かれていますが。

Go Modules Reference - The Go Programming Language

言語仕様には、モジュールのことは書いてなさそうなんですよね…。

The Go Programming Language Specification - The Go Programming Language

環境

今回の環境は、こちら。

$ go version
go version go1.15.6 linux/amd64

お題

以下のようなディレクトリツリーを作ります。

├── calc
│   ├── func.go
│   └── go.mod
├── format
│   ├── func.go
│   └── go.mod
├── go.mod
└── main.go

最上位がmainモジュールで、以下の依存関係で呼び出しができるようにしてみます。

  • maincalc
  • mainformatcalc

※ あとで気づきましたが、入れ子にしない方が例として適切でしたね
※ というか、この構成なら単純にパッケージにしてimportすれば良かったと後で知る…

では、始めていきましょう。

ローカルのモジュール(パッケージ)を呼び出す

最初に、モジュール用のディレクトリを作って移動。

$ mkdir import-local-package-example
$ cd import-local-package-example

go mod init

$ go mod init example.com/my/import-local-package-example
go: creating new go.mod: module example.com/my/import-local-package-example

モジュール名には、慣習的に(公開時を踏まえると)ホスト名を入れた方が良さそうなので、今回はexample.com
しておきます。ふだんはgithub.com/[ユーザー名]あたりが使われることが多いのでしょう。

module path:

簡単なソースコードを書いて
main.go

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World!!")
}

確認。

$ go run main.go
Hello World!!

この時の、go.modはこうです。
go.mod

module example.com/my/import-local-package-example

go 1.15

続いて、呼び出し先のモジュールを作ります。

$ mkdir calc
$ cd calc

go mod init

$ go mod init example.com/my/import-local-package-example/calc
go: creating new go.mod: module example.com/my/import-local-package-example/calc

こんな感じのファイルを作成。
func.go

package calc

func Plus(a int, b int) int {
    return a + b
}

func Minus(a int, b int) int {
    return a - b
}

main側に戻ります。

$ cd ..

現時点での構成は、こんな感じです。

$ tree
.
├── calc
│   ├── func.go
│   └── go.mod
├── go.mod
└── main.go

次に、main.goを雰囲気で先ほど作ったcalcモジュールを使うように変更してみます。
main.go

package main

import (
    "./calc"
    "fmt"
)

func main() {
    //fmt.Println("Hello World!!")

    fmt.Printf("1 + 3 = %d\n", calc.Plus(1, 3))
    fmt.Printf("5 - 2 = %d\n", calc.Minus(5, 2))
}

実行。

$ go run main.go 
build command-line-arguments: cannot find module for path _/path/to/import-local-package-example/calc

そんなモジュールはないよ、と言われます。

importを、go mod init時に指定したものに変えてみましょう。

import (
    //"./calc"
    "example.com/my/import-local-package-example/calc"
    "fmt"
)

実行。

$ go run main.go
go: finding module for package example.com/my/import-local-package-example/calc
main.go:5:2: cannot find module providing package example.com/my/import-local-package-example/calc: unrecognized import path "example.com/my/import-local-package-example/calc": reading https://example.com/my/import-local-package-example/calc?go-get=1: 404 Not Found

これだと公開されているモジュールを期待することになってしまうので、当然ですが世の中にそんなモジュールはありません。

ここで、go.modをこちらから
go.mod

module import-local-package-example/main

go 1.15

以下のように変更します。
go.mod

module example.com/my/import-local-package-example

go 1.15

require example.com/my/import-local-package-example/calc v0.0.1

バージョンは、適当に振りました。

これだけだと、依存関係をgo.modに明記しただけなので、go mod edit -replaceを行ってローカルパスで解決するようにします。

# Add a replace directive.
$ go mod edit -replace example.com/a@v1.0.0=./a

Go Modules Reference / go mod edit

実行。

$ go mod edit -replace example.com/my/import-local-package-example/calc@v0.0.1=./calc

すると、go.modに1行追加されます。今回はgo mod edit -replaceを使いましたが、手でgo.modを書き換えてもOKです。
go.mod

module example.com/my/import-local-package-example

go 1.15

require example.com/my/import-local-package-example/calc v0.0.1

replace example.com/my/import-local-package-example/calc v0.0.1 => ./calc

このやり方は、以下に記載があります。

Go Modules Reference / replace directive

Go Modules / Can I work entirely outside of VCS on my local filesystem?

これで、やっと実行できるようになります。

$ go run main.go
1 + 3 = 4
5 - 2 = 3

めでたし、めでたし。

疑似バージョン

ところで、モジュールに対する依存関係を書く時にバージョンが必要になるのですが、ここで疑似バージョン(Pseudo Version)を
使うことでVCS環境内で依存関係を解決できるようです。

Go Modules Reference / Pseudo-versions

こうすると、replaceは要らなくなりそうですね。

疑似バージョンは、「ベースバージョン-タイムスタンプ-コミットハッシュ」で構成されます。

こんな感じですね。

  • vX.0.0-yyyymmddhhmmss-abcdefabcdef
  • v1.2.4-0.20191109021931-daa7c04131f5

もうひとつパッケージを増やす

続いて、もうひとつパッケージを増やしてみましょう。先ほどmain内に書いていた処理を移すことにします。

ディレクトリの作成。

$ mkdir format
$ cd format

go mod init

$ go mod init example.com/my/import-local-package-example/format
go: creating new go.mod: module example.com/my/import-local-package-example/format

ソースコードを作成。先ほど作った、calcパッケージを使います。
func.go

package format

import (
    "example.com/my/import-local-package-example/calc"
    "fmt"
)

func FormatPlus(a int, b int) string {
    return fmt.Sprintf("%d + %d = %d", a, b, calc.Plus(1, 3))
}

func FormatMinus(a int, b int) string {
    return fmt.Sprintf("%d - %d - %d", a, b, calc.Minus(5, 2))
}

go.modの中身は、このままです。
go.mod

module example.com/my/import-local-package-example/format

go 1.15

main側に戻って

$ cd ..

ソースコードを修正。calcパッケージへの依存は削除します。
main.go

package main

import (
    //"./calc"
    //"example.com/my/import-local-package-example/calc"
    "example.com/my/import-local-package-example/format"
    "fmt"
)

func main() {
    //fmt.Println("Hello World!!")

    //fmt.Printf("1 + 3 = %d\n", calc.Plus(1, 3))
    //fmt.Printf("5 - 2 = %d\n", calc.Minus(5, 2))

    fmt.Println(format.FormatPlus(1, 3))
    fmt.Println(format.FormatMinus(5, 2))
}

go.modですが、2つのモジュールへの依存とreplaceを書きます。
go.mod

module example.com/my/import-local-package-example

go 1.15

//require example.com/my/import-local-package-example/calc v0.0.1

//replace example.com/my/import-local-package-example/calc v0.0.1 => ./calc

require (
    example.com/my/import-local-package-example/format v0.0.1
    example.com/my/import-local-package-example/calc v0.0.1
)

replace (
    example.com/my/import-local-package-example/format v0.0.1 => ./format
    example.com/my/import-local-package-example/calc v0.0.1 => ./calc
)

formatモジュールのgo.modに、calcモジュールへの依存関係を書いても意味がありません。エントリポイントとなる位置にある
go.modに書いてあることが重要みたいです。

推移的依存関係まで、全部書く必要があるということですね。

最終的に、ディレクトリ構成はこうなりました。

$ tree
.
├── calc
│   ├── func.go
│   └── go.mod
├── format
│   ├── func.go
│   └── go.mod
├── go.mod
└── main.go

動作確認。

$ go run main.go 
1 + 3 = 4
5 - 2 - 3

OKですね。

ビルド&実行

最後に、ビルドして

$ go build -o app main.go

実行。

$ ./app 
1 + 3 = 4
5 - 2 - 3

こちらもOKです。