Go の Workspace を試す

## はじめに
本記事は [株式会社 RetailAI X Advent Calendar 2022](https://qiita.com/advent-calendar/2022/retail-ai-x) の24日目の記事です。
昨日の記事は @takaomag さんの [threadと一貫性とAtomic](https://qiita.com/takaomag/items/cbf8301709024e6073fa) でした。
まだ見られていない方は是非ご確認を！


## 何に関する記事か
Go 言語の Workspace について書いていきます。
この機能は Go 1.18 から導入されており、リリースされた2022年3月から正式に利用できています。
([こちらのリリースノート](https://go.dev/doc/go1.18#go-work) に載ってます)
ただ、同じタイミングで Generics や Fuzzing がリリースされており、
さほど調べずにここまできてしまったので、この機会にまとめてみます。
以下の内容は Go の基礎を知っていることが前提となっています。


## 環境
- MacOS Ventura Version 13.1(M1)
- go version go1.19.4 darwin/amd64


## Workspace とは?
そもそも Go での Workspace とは何でしょうか。
Workspace とは、複数のモジュールを同時に扱って開発をする際に楽をするためのツールです。
これを使うことで、ローカル環境の依存関係の解決が楽になります。


## 利用例
単純な利用例を考えてみます。
適当なリポジトリを用意し、calc というパッケージを作ります。
そこに int 型の数値を合計する ```Sum``` という関数を定義します。

``` calc/calc.go
package calc

func Sum(nums []int) int {
	var total int
	for _, num := range nums {
		total += num
	}
	return total
}
```

次に、calc/calc.go とは別のモジュールを作成し、```Sum``` を利用します。
ここでは適当に example ディレクトリとその中に main.go を置いています。

``` example/main.go
package main

import (
	"fmt"

	"github.com/k-yoshigai/example-calc-go/calc"
)

func main() {
	nums := []int{1, 2, 3}
	fmt.Printf("result: %v\n", calc.Sum(nums))
}
```

go get コマンドの実行が必要ですが、問題なくコンパイルできます。

ここで、int だけでなく他の型でも合計値を出力しなくてはならなくなったと仮定します。
具体的には下記のような感じです。

``` example/main.go
func main() {
	nums := []float64{1.1, 2.2, 3.3}
	fmt.Printf("result: %v\n", calc.Sum(nums))
}
```

もちろん、現状の ```Sum``` は引数に int のスライスしか受け付けないため、上記のコードはコンパイルできません。

```
./main.go:12:38: cannot use nums (variable of type []float64) as type []int in argument to calc.Sum
```

```Sum``` の修正例として、Generics を利用して下記のように直すことができます。

``` calc/calc.go
package calc

type Number interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](nums []T) T {
	var total T
	for _, num := range nums {
		total += num
	}
	return total
}
```

ここで、特に何も気にせずに修正をしようとすると、Sum 関数の内容を修正してプッシュし、
その修正内容をもとに example/main.go 側の動作を確認する、となるかもしれませんが、
可能なら呼び出し側の動作確認を終えてからプッシュしたいかと思います。

もちろん、Workspace を使わずともこの問題は解決できます。
例として、example 側の go.mod に replace ディレクティブを追記する、というやり方が考えられます。
1. example ディレクトリと同じ階層に example-calc-go リポジトリをクローンする
2. calc.go を修正する
3. example 側の go.mod に下記を追加する
```
replace github.com/k-yoshigai/example-calc-go => ../example-calc-go
```
ディレクトリ構成のイメージです。
```
.
├── example
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── example-calc-go
    ├── calc
    │   ├── calc.go
    │   └── calc_test.go
    └── go.mod
```

しかし、この go.mod の修正はローカルでの開発のためだけであり、
通常はこれはプッシュしたいものではないかと思います。
このような場合に Workspace は便利です。

## Workspace の使い方
前提が長くなりましたが、Workspace の使い方に入ります。簡単です。
Workspace を使うには、まずは ```go work init``` コマンドを実行します。

```
❯ go work init
```

実行後、カレントディレクトリに go.work というファイルが作成されます。
```
.
├── example
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── example-calc-go
│   ├── calc
│   │   ├── calc.go
│   │   └── calc_test.go
│   └── go.mod
└── go.work              <- これ
```
これは Workspace の管理用ファイルで、これに対して Workspace に含めたいディレクトリを指定します。

ディレクトリを追加するには ```go work use [追加したいディレクトリのパス]``` を実行します。
これにより use ディレクティブが追加され、どのディレクトリを Workspace に追加するかを Go に伝えます。
スペースで区切って複数ディレクトリを一度に追加できます。また、パスは相対パスで指定します。なので上記のツリーの example-calc-go ディレクトリは example ディレクトリと同じ階層にある必要はありません。
(```go work init ./example ./example-calc-go``` のように最初の init のタイミングでまとめて追加することも可能です)

```
❯ go work use ./example ./example-calc-go
```

コマンド実行後の go.work は下記のようになります。

```
go 1.19

use (
        ./example
        ./example-calc-go
)
```

この状態で ```Sum``` を修正し、go.mod に追加した replace ディレクティブを消した上で、呼び出し側のコードを実行してもエラーが出ることなく実行できます。

```
❯ go run example/main.go
result: 6.6
```

正しく結果が出力されました。
Workspace を利用して go.mod の修正なしでローカルの ```Sum``` を参照できています。


## まとめ
今回は Go の Workspace について書いてみました。
マルチモジュールな開発でも go.mod の修正が不要になるのは良い気がします。
明日は @tou_kenichiro さんの記事になります。
引き続きお楽しみください！


## 参考
- [Go 公式のチュートリアル](https://go.dev/doc/tutorial/workspaces)
- [Go 公式ブログの説明記事](https://go.dev/blog/get-familiar-with-workspaces)
- [Go Modules のリファレンス](https://go.dev/ref/mod#workspaces)