LoginSignup
5
4

More than 1 year has passed since last update.

Go言語の初学者が go mod を使って自分で作ったローカルのモジュールを引っ張ってこれるようにするまで【Go Modules】

Last updated at Posted at 2022-07-29

新参者なので 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から

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_practice/go.mod
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
greetings.go
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
}
greetings/go.mod
module local.package/greetings

go 1.18

main.goの編集

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_practice/go.mod
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 したいと思います。

hello/hello.go
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を引っ張ってこれました。

hello/go.mod
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を引っ張る

下のように編集

main.go
package main

import (
	"fmt"

	"local.package/hello"
)

func main() {
	message := hello.Hello("ポカホンタス")
	fmt.Println(message)
}
modules_practice/go.mod
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
image.png

詳しく理解できてないので詳細は割愛しますが、依存先の依存先(つまりgreetings.go)のバージョンも取得した上で hello.go を呼び出さないといけません。

go moduleのreplaceでハマったこと - Zenn(森に帰省中のゴリラ) がMVSについてもう少し深堀しています。

今回はローカル上でバージョンの取得も行うため replace を使っています。

image.png
※ R が replace で任意の依存先を指定している図

すなわち、

modules_practice/go.mod
module local.package/main

go 1.18

replace local.package/greetings => ./greetings // greetingsも置換対象としてローカルでバージョン指定する
replace local.package/hello => ./hello

$ go mod tidy
modules_practice/go.mod
// ~ 省略 ~

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 ~

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4