Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法

この記事は 株式会社 ACCESS Advent Calendar 2019 16 日目の記事です。

昨日は、@Momijinnさんが楽しげな記事を書いてくれました。

今日は、すこしマニアックな話になってしまいます。
go mod でインターネット上のレポジトリにはないローカルパッケージを扱いたいときに、go.mod の replace directive を使えばいいらしいことは分かったのですが、やってみると色々とハマったので、その時に学んだことや調べたことを記録したいと思います。

ここで記述する内容は、 go 1.13 のバージョンを前提としています。

---

## 結論

*Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法*

まずはじめに、結論だけ知りたい人向けに、どうすればいいのかを記述しておきます。

以下のようなファイル構造を用意して、

```
sample
├── go.mod
├── go.sum
├── goodbye
│   ├── go.mod
│   └── goodbye.go
└── hello
    └── hello.go
```

(hello.go の中から、ローカルパッケージの goodbye.go を使う想定です。)

`sample/go.mod` で以下のように、記述します。

```go:go.mod
module example.com/me/hello

go 1.13

replace local.packages/goodbye => ./goodbye
```

`module example.com/me/hello` : ローカルパッケージを使う側のモジュール名。
`go 1.13` : go のバージョン
`local.packages/goodbye` : ローカルパッケージのモジュール名（任意だが**パスのルート部分（ドメイン名に該当する箇所）にドットが含まれている必要があります**）
`replace local.packages/goodbye => ./goodbye` : 左側のモジュールを、右に指定した（ローカル上の）相対パスに置き換えるように指定しています。
（ローカルパッケージ googbye は、sample レポジトリの外部にあっても構いません。）

以下のように、ソースファイルで import する時は、 **go.mod で記述したモジュール名を指定する必要があります**（絶対パスや相対パスで記述しても import 先を見つけてくれません）。

```go:hello.go
import "local.packages/goodbye"
```

また、**ローカルパッケージのディレクトリ `./goodbye` にも、 go.mod が配置されている必要があります**（touchで作った空ファイルでも動く）。
* 上述のファイルツリーで言うところの、`sample/goodbye/go.mod`。
モジュール名と、ローカルのディレクトリ名は、必ずしも一致していなくても動きます。

以上を行うことで、go modを使って、インターネット上のレポジトリにはないローカルパッケージを import することができます。

なお、上記の条件を満たすサンプルを、以下に公開しました。
[GitHub - hnishi/zakurero_test_gomodules](https://github.com/hnishi/zakurero_test_gomodules)

---

### 解説

go mod は、import されるモジュールがインターネット上のレポジトリで管理されることが前提とされていると思われます。
そのため、go.mod の require directive では、github.com などのリポジトリホストのドメイン名が含まれている必要があります。
この判定を go mod は、パスの先頭にドットが含まれているかどうかで判断しているようです。
したがって、ローカルパッケージを利用したい場合は、そのモジュール名にドットを含めて replace で指定する必要があります（例えば、local.packages）。

go modが登場する以前は、 import に直接、絶対パスや相対パスを書くことが許容されていました。
なお、 Module-aware mode (go mod による管理有効化) に対して、古い GOPATH に依存パッケージを探しにいく方法を、 GOPATH mode と呼ぶそうです。

> If GO111MODULE=off, then the go command never uses module support. Instead it looks in vendor directories and GOPATH to find dependencies; we now refer to this as "GOPATH mode."

[go - The Go Programming Language](https://golang.org/cmd/go/#hdr-Module_support)

パスのルート部分（ドメイン名該当する箇所）にドットが含まれている必要がある理由を、公式ドキュメント等で探してみましたが、見つからず。
おそらく、インターネット上のレポジトリで管理されることが前提（必須レベル）にされていると思われます。

パッケージのディレクトリにgo.modを置く必要がある理由としては、これもドキュメント等で記述を見つけられませんでした。
おそらく、go modで import されるモジュール（パッケージ）は、同じように go.mod で管理されるべきという方針があるのだと思います。

## まとめ

Go Modules でインターネット上のレポジトリにはないローカルパッケージを import する方法に関して記述しました。
明日は、@knagauchiさんが「とりあえず、今の作業が間に合えばKotlinの話、間に合わなければ今年プロジェクトで実践したタスク管理術の話です」をしてくださるそうです。
お楽しみに！

# Appendix

## Go Modules 概要

- Go Modulesは、go1.11から導入されたGo標準のGoパッケージ管理ツールである
- Go1.11時点では試験的に導入されており、feedbackを収集してgo1.14にて完成が予定されている
    - > Modules are an experimental opt-in feature in Go 1.11, with the plan of incorporating feedback and finalizing the feature for Go 1.14.
    - https://github.com/golang/go/wiki/Modules

## go mod 使用方法

- Module-aware modeの有効化
    - GO111MODULE=on : 有効化（go.modがないと動かない）
    - GO111MODULE=off : 無効化（依存パッケージをGOPATHに探しに行く）
    - GO111MODULE=auto : go.modが カレントディレクトリ or 上流ディレクトリに存在している場合は有効化、ない場合は無効化
    - go1.13では、GO111MODULE=autoがデフォルト
    - https://golang.org/cmd/go/#hdr-Module_support
- go.modの作成
    - go.modを作成したいディレクトリへ移動
    - `go mod init <作成するモジュール名>`
    - go.modが生成されると、go buildなどのgo commandsを実行した際に自動でgo.modに最新の依存パッケージが追記される（go.sumも生成される）
        - > Once the go.mod file exists, no additional steps are required: go commands like 'go build', 'go test', or even 'go list' will automatically add new dependencies as needed to satisfy imports.
        - https://golang.org/cmd/go/#hdr-Defining_a_module
- go.modの整理
    - `go mod tidy`で、go.modに必要なパッケージの追記、使われていないパッケージの削除することができる
    - https://golang.org/cmd/go/#hdr-Add_missing_and_remove_unused_modules
- 既にgo.modが存在する場合
    - go build実行時に、go.modに記載のバージョンが、インストールされる
      - 事前に go get や go install する必要はない
- go.sumの使い方
    - モジュールのauthenticationに使われる（自動で行われる）
    - go commandsにより自動生成される
    - https://golang.org/cmd/go/#hdr-Module_authentication_using_go_sum
- go.modの記述方法
    - 3つのdirectiveを使用する
    - require: to require a particular module at a given version or later
    - exclude: to exclude a particular module version from use
    - replace: to replace a module version with a different module version
    - ローカルモジュールを使用するために、replaceを使う
        - replace <モジュール名> => ローカルに置いたモジュールのソースまでの絶対パス
            - 相対パスを指定する場合は、`./ or ../`をPATHの先頭に記載する
    - https://golang.org/cmd/go/#hdr-The_go_mod_file
- 依存パッケージのアップグレード
    - 特定のパッケージ: `go get example.com/package`
    - 全てのパッケージ: `go get -u example.com/package`
    - https://github.com/golang/go/wiki/Modules#how-to-upgrade-and-downgrade-dependencies

### go mod ハマりポイント

- モジュール名のルートには、ドット（.）が含まれていなければならない
- replace directiveの右側には絶対パスを指定する
    - 相対パスを指定する場合は、`./ or ../`をPATHの先頭に記載する
- ローカルに置いてあるパッケージを使う場合は、そのパッケージのディレクトリにgo.modを置く必要がある（touchで作った空ファイルでも動く）
    - モジュール名と、ローカルのディレクトリ名は一致していなくても良い
- go buildするときに読み込まれるgo.modは、ワークディレクトリから１個ずつ上のディレクトリへ遡っていき、一番最初に見つかったgo.modが使われる
    - > When the go command is run, it looks in the current directory and then successive parent directories to find the go.mod marking the root of the main (current) module.
    - go buildするワークディレクトリを変えることで、go.modを使い分けることができる
- ローカルモジュール等でバージョンがない場合は、go.modファイルのrequireで、v0.0.0を指定する
    - > Untagged revisions can be referred to using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef, where the time is the commit time in UTC and the final suffix is the prefix of the commit hash. 
    - https://golang.org/cmd/go/#hdr-Pseudo_versions
- > 現状の go mod は git tag と git branch の区別が付けれない
    - ブランチ名に、v2.0.0とか作ると、それを最新バージョンと勘違いして`go get -u`で取ってきてしまう
    - > https://github.com/mattn/go-sqlite3/issues/755

## ドキュメントから要点を引用

- Goにおいて、モジュールとはパッケージの集合のことを指す、新しいコンセプトらしい。

> A module is a collection of related Go packages that are versioned together as a single unit.

レポジトリ、モジュール、パッケージの関係

- １つのレポジトリは、１つ以上のモジュールを含む
- それぞれのモジュールは、１つ以上のパッケージを含む
- それぞれのパッケージは、１つのディレクトリに、１つ以上のGoソースファイルを含む

> Summarizing the relationship between repositories, modules, and packages:
> 
> - A repository contains one or more Go modules.
> - Each module contains one or more Go packages.
> - Each package consists of one or more Go source files in a single directory.

[Modules · golang/go Wiki · GitHub](https://github.com/golang/go/wiki/Modules#modules)

- モジュールという概念について

> A module version is defined by a tree of source files, with a go.mod file in its root. When the go command is run, it looks in the current directory and then successive parent directories to find the go.mod marking the root of the main (current) module.

- go mod initは、depなどのツールの設定を取り込んでくれる

> In a project already using an existing dependency management tool like godep, glide, or dep, 'go mod init' will also add require statements matching the existing configuration.

- GOPATHについて

> When using modules, GOPATH is no longer used for resolving imports. However, it is still used to store downloaded source code (in GOPATH/pkg/mod) and compiled commands (in GOPATH/bin).

[go - The Go Programming Language](https://golang.org/cmd/go/#hdr-The_go_mod_file)

## 実験

go mod の挙動を調べるために、実験を行った。

[GitHub - hnishi/zakurero_test_gomodules](https://github.com/hnishi/zakurero_test_gomodules)

### モジュール名にドットが含まれていない場合のエラー

```go
import "localpackage/goodbye"
```

```
$ go run hello.go
build command-line-arguments: cannot load goodbye: malformed module path "goodbye": missing dot in first path element
```

### goodbye/にgo.modがない場合のエラー

```
$ go run hello.go
go: local.packages/goodbye@v0.0.0: parsing ../goodbye/go.mod: open /path/to/zakurero_test_gomodules/goodbye/go.mod: no such file or directory
```

### importを相対パスで指定した場合

goodbyeというローカルパッケージを使いたい。
Go Modulesを使わない場合には、相対パスでimportすれば良かったが、Go Modulesではそれが禁止されている。
Module-aware modeがonのときは、モジュール名で指定しなければならない。
試しに、相対パスで指定したときのエラーは以下。

```go:hello2.go
    package main

    import (
     "fmt"
     "../goodbye"
    )

    func main() {
     fmt.Println(goodbye.Goodbye())
    }
```

```
$ GO111MODULE=on go run hello2.go
build _/path/to/zakurero_test_gomodules/goodbye: cannot find module for path _/path/to/zakurero_test_gomodules/goodbye
```

Module-aware modeがoffのときは、成功する。

```
$ GO111MODULE=off go run hello2.go
Goodbye
```

### このレポジトリをgo getできるか試してみた

```
$ GO111MODULE=on go get github.com/hnishi/zakurero_test_gomodules

go: finding github.com/hnishi/zakurero_test_gomodules latest
go get: github.com/hnishi/zakurero_test_gomodules@v0.0.0-20191215082227-606721d8e919 requires
        local.packages/goodbye@v0.0.0: unrecognized import path "local.packages/goodbye" (https fetch: Get https://local.packages/goodbye?go-get=1: dial tcp: lookup local.packages on 192.168.65.1:53: no such host)
```

https://local.packages/goodbye が見つからないと、言っている。
go.mod の replace で記述した、相対パス ./goodbye は、 go get では、よしなにしてくれないようである。
ダウンロードは、してくれている。

```
$ ls ${GOPATH}/pkg/mod/github.com/hnishi/zakurero_test_gomodules\@v0.0.0-20191215082227-606721d8e919
LICENSE  README.md  go.mod  go.sum  hello  zakurero_test_gomodules.go
```

## 公式ドキュメントリンク

https://github.com/golang/go/wiki/Modules
https://golang.org/cmd/go/#hdr-The_go_mod_file