Go1.18からのWorkspace modeをさっそく使ってみた
はじめに
先日mercariさんの「Online Spring Internship for Gophers 2022」に参加し、Go1.18で追加された新機能について学びました。
資料はtenntennさんがスライドを公開されているので、興味がある方はそちらも見てみてください。
その中で、参加者がそれぞれ個人でGo1.18に関係するOSSを開発するパートがありました。
自分はそこでFuzzingできる関数を表示するshowfuzzとFuzzテストのtemplateを生成するgofuzzgenというツールを開発しました。
開発時に早速Go1.18からの新機能の1つであるWorkspace modeを用いたので、概要と使ってみた感想について書いていきます。
Workspace modeとは?
まず、「Workspace modeって何?」という方もいらっしゃると思うので、簡単にWorkspace modeについて説明していきます。
概要をご存じの方は、「Workspace modeの機能」のところまでいってください。
これまで
Go1.17以前はマルチモジュールでの開発中にもし使用しているmoduleに変更があった場合、毎度タグを打ってリリースする必要がありました。
タグを打ってリリースという作業を毎回することを避けるために、go.modのreplaceディレクティブを用いるという手段がありました。その例を下に示します。
$ tree .
.
├── moduleA
│ ├── go.mod
│ └── moduleA.go
└── moduleB
├── go.mod
└── main.go
このようなディレクトリ構成になっており、moduleA/moduleA.goを見てみるとHelloA
という関数が実装されていました。
package moduleA
import "fmt"
func HelloA(name string) string {
return fmt.Sprintf("Hello, %s from mouduleA", name)
}
そしてこの関数をmoduleBの中で使用してみます。
package main
import (
"fmt"
"example.com/moduleA"
)
func main() {
name := "moduleB"
fmt.Println(moduleA.HelloA(name))
}
これを実行してみると(明示的にgo1.17を使うようにしています)、
$ pwd
/.../go/src/moduleB
$ go1.17 run .
main.go:3:8: package modulea is not in GOROOT (/usr/local/go/src/moduleA)
このように表示されます。moduleAは実装したばかりでリリースしていないので当然moduleBで使えません。
ここでreplaceディレクティブの出番です。go.mod内にreplace example.com/moduleA = ../moduleA
を追加し、go1.17 mod tidy
を実行します。
module example.com/moduleB
go1.17
replace example.com/moduleA => ../moduleA
require example.com/moduleA v0.0.0-00010101000000-000000000000
そうすると、このようになります。これを実行すると、
$ go1.17 run .
Hello, moduleB from moduleA
うまく表示できました。
しかし、このreplaceにはいくつか気を付けないといけない点がありました。
それはローカル環境がそれぞれ違うので、コミット時には削除する必要があるという点です。
しかし、存在を忘れてpushしてしまったという経験がある方が多いと思います。これを解消するのがWorkspace modeです。
Workspace modeの使い方
ではworkspace modeを使って先ほどの例と同じことをやっていきます。
Workspace modeを使うためには、まずworkspaceとなるディレクトリを用意する必要があります。
$ mkdir example-workspace
そしてその中に先ほどの例と同じディレクトリ構成を用意します。実際に使うときはgit clone
を使うことになると思います。
example-workspace
├── moduleA
│ ├── go.mod
│ └── moduleA.go
└── moduleB
├── go.mod
└── main.go
そして、example-workspaceにいる状態で、go1.18 work init moduleA moduleB
(ここでも明示的にgo1.18でコマンドを使用)を実行します。
$ go1.18 work init moduleA moduleB
$ ls
go.work moduleA moduleB
go.work
の中身は以下のようになっています。
go 1.18
use (
./moduleA
./moduleB
)
この状態でgo1.18 run moduleB/main.go
を実行すると、
$ go1.18 run moduleB/main.go
Hello, moduleB from moduleA
うまく表示することができました。ここでmoduleA/moduleA.go
に変更を加えてみます。
package moduleA
import "fmt"
func HelloA(name string) string {
return fmt.Sprintf("Hello, %s from Workspace mode", name)
}
go1.18 run moduleB/main.go
を再度実行してみると、
$ go1.18 run moduleB/main.go
Hello, moduleB from Workspace mode
変更が反映されていることがわかります。これがWorkspace modeの基本的な使い方になります。
Workspace modeの機能
ここからはWorkspace modeの他の機能について見ていきます。コマンドはinitの他に3種類存在します。
- edit
- sync
- use
それでは順に見ていきます。
edit
editはgo.workファイルを編集するために用います。機能が多いので大雑把に説明します。
go1.18 work edit -use [ディレクトリ名]
で使用するディレクトリを追加することができます。これは存在しないディレクトリを指定しても追加されます。
go1.18 work edit -dropuse [ディレクトリ名]
で不要なmoduleをgo.workファイルから削除できます。
go1.18 work edit -replace old[@v]=new[@v]
で指定されたmodule pathとバージョンのペアを指定することができます。つまり、今までのreplaceディレクティブを作成することができます。
$ go1.18 work edit -replace moduleA=./moduleA
$ cat go.work
go1.18
replace moduleA => ./moduleA
go1.18 work edit -dropreplace=old[@v]
はreplaceを削除することができます。
$ go1.18 work edit -dropreplace moduleA
go1.18 work edit -fmt
はその名の通り、formatをかけてくれます。これは使用していないmodule pathを削除してくれるといった便利なものではなく、文法上おかしいところを直してくれます。
$ cat go.work
go 1.18
use (
./moduleA
./moduleB
)
$ go1.18 work edit -fmt
$ cat go.work
go1.18
use (
./moduleA
./moduleB
)
go1.18 work edit -print
はedit実行時に、実行後のgo.workファイルを標準出力に表示します。変更はなされずに、どのようになるかだけを表示します。
$ go1.18 work edit -print -use moduleC
go1.18
use (
./moduleA
./moduleB
./moduleC
)
$ cat go.work
go1.18
use (
./moduleA
./moduleB
)
-print
だけをflagに指定した場合、現在のgo.workファイルを標準出力に表示します。
go1.18 work edit -json
はjson形式でedit実行時に、実行後のgo.workを標準出力に表示します。こちらも実行結果を表示して、実際には変更がなされません。
$ go1.18 work edit -use moduleC -json
{
"Go": "1.18",
"Use": [
{
"DiskPath": "./moduleA"
"ModPath": "example.com/moduleA"
},
{
"DiskPath": "./moduleB",
"ModPath": "example.com/moduleB"
},
{
"DiskPath": "./moduleC"
},
],
"Replace": null
}
$ cat go.work
go1.18
use (
./moduleA
./moduleB
)
存在しないディレクトリを追加すると、ModPath
が空になっていることが分かります。
-print
と同様に、-json
単体で実行すると現在のgo.workファイルを標準出力に表示します。
-go
は使うGoのバージョンを変えることができます。go mod tidy
の-go
と同じです。
sync
go1.18 work sync
はすべての依存関係を最小バージョン選択を用いてworksapce's build list
(go.workファイルのuse()内のもの)を生成する機能になっています。
use
useは新しいmoduleをWorkspaceに使いたいときに使用します。
$ go1.18 work use moduleC
flagに-r
を持っており、再帰的にディレクトリを探索することが可能です。
これはinitでも使用できます。
example-workspace
├── go.work
├── moduleB
│ ├── go.mod
│ └── main.go
└── modules
├── moduleA
│ ├── go.mod
│ └── moduleA.go
└── moduleC
├── go.mod
└── moduleC.go
$ go1.18 work use -r modules
$ cat go.work
go1.18
use (
./moduleB
./modules/moduleA
./modules/moduleC
)
Workspace modeを使ってみた感想
良かった点
良かった点は変更があった際にいちいちリリースしなくてよくなったことです。
毎度リリースしたり、replaceディレクティブで書き換える必要がなくなったので、開発速度が向上しました。
それに加えてレビューのときにreplaceディレクティブがあることを指摘され、直してpushしてもう一度pipelineが通るのを待つということをしなくてよくなります。
特にタイポに気付いたときなど、細かなリファクタリングのときにWorkspace modeの良さを実感しました。
気を付けないといけない点
気を付けないといけないことは、最終的には参照しているmoduleをgo getで対象のバージョンにする必要があることです。
公式のチュートリアルのFuture stepというところにも書いてあります。なのでgitにpushする際や、リリースする時には注意が必要です。
自分はこれに気付いていなくて、いざタグを打ってリリースし、開発したツールを実行してみたところコンパイルエラーがでて正常に動かすことができなかったです。
そのため、仮にgo.workファイルをモノレポに含めてマルチモジュールのGo開発をしていても、開発時は楽にはなるとは思いますが既存のパッケージの依存関係が解消されることはないので、注意が必要です。
おわりに
いかがだったでしょうか。Workspace modeはマルチモジュールの開発速度を向上させるのにとても役立つということがわかったと思います。
自分の書いたことの中に間違っていることが含まれていることもあると思うので、その時は優しく教えていただけたら幸いです。
Discussion