Goの複数バージョンをインストールして切り替える

:::note info
この記事は[ZOZO AdventCalender 2023](https://qiita.com/advent-calendar/2023/zozo)シリーズ4の6日目の記事です。
:::

:::note warn
この記事の内容は古い情報です。  
Go1.21から導入された[GOTOOLCHAIN](https://go.dev/doc/toolchain)を使うことで、複数バージョンのGo言語をインストールする必要がなくなりました。  
:::

# 始めに
go言語で複数バージョンをPCにインストールして切り替える方法について説明します。
なおgo言語では[Go1.x系の間には後方互換性を破壊するような変更は加えないということがルール化](https://go.dev/doc/go1compat)されているので、
基本的には最新バージョンを使用すれば良く、複数バージョンをPCに入れる必要はありません。
後方互換性を破壊するような変更を加えないといけないといった状況になった際にはGo2としてリリースされるという方針になっているようです。
しかしながら諸般の事情で複数バージョンをPCに入れて切り替えたいということはありえます。
筆者も最新バージョンではビルドができないというプロジェクトに最近関わることになり、最新バージョンとは別に1.17系を入れる必要が出てきて複数バージョンを扱う方法を調べました。
この記事ではそこで得た知見を共有したいと思います。

# 複数バージョンをインストールする方法
一昔前までは他の言語でもあるようなxxenvというような名前のツールでgoenvが人気があった印象ですが、現在では開発が止まっているようであまりオススメできません。
しかしながら実は[公式ドキュメントで複数バージョンをインストールする方法について説明](https://go.dev/doc/manage-install)しているものがあります。
公式が安定なのは間違いないのでここは素直に公式ドキュメントで説明されてる方法で複数バージョンのインストールをすることにします。

## install
1.17.13をインストールしたい場合、以下のコマンドを実行します。
※他のバージョンを入れたい場合はバージョン番号部分(1.17.13)を入れたいバージョンのものにすれば良いです。
```bash
$ go install golang.org/dl/go1.17.13@latest
$ go1.17.13 download
```
このコマンドを実行すると`~/sdk`以下に指定したバージョンのgoがインストールされます。
```bash
$ ls -l ~/sdk
total 0
drwxr-xr-x  21 xxxxx  staff  672 11  2 15:37 go1.17.13
$ 
```
インストール可能なバージョンは[download page](https://go.dev/dl/)で確認することができます。

## 切り替え
公式のやり方でインストールしたgoコマンドは、goコマンドの末尾にインストールしたバージョン番号を追加したコマンドで実行することになります。
```bash
$ go1.17.13 version
go version go1.17.13 darwin/arm64
$
```
なるほど確かにこれなら複数バージョンを扱えますね！と言いたいところですが、
プロジェクトでビルドスクリプトやMakefileを用意している場合、goコマンドの末尾にバージョン番号を追加していたりはしないので正直使いづらいです。
goコマンド自体を切り替えることができれば使いやすくなりそうなので、やり方を考えていきます。

## goコマンドを差し替える
`GOROOT`という環境変数があるのでこれを切り替えればOKです。
昔は公式ドキュメントに`GOROOT`についての記述があった気がするのですが、現在の公式ドキュメントでは[ソースインストールのドキュメントで触れられてる](https://go.dev/doc/install/source#:~:text=%24-,GOROOT,-The%20root%20of)のみでした。
この環境変数はGo SDKのpathを設定するものなので、複数バージョンを扱うといったユースケースで使うものになり、まさに今回のケースで使うための環境変数です。
切り替えは以下のようにやります。
```bash
$ export GOROOT=`go1.17.13 env GOROOT`
$ export PATH=$GOROOT/bin:%PATH
$ go version
go version go1.17.13 darwin/arm64
$
$ which go
/Users/xxxxx/sdk/go1.17.13/bin/go
$
```
go1.17.13で設定されているGOROOT(`~/sdk/go1.17.13`になるはず)を取得し、GOROOTに設定して切り替えます。
go1.17.13のbinをPATHに追加するのも忘れずに。
これでgoコマンドの実体が`~/sdk/go1.17.13/bin/go`になったはずです。

## 面倒くさい
goコマンドの差し替えはできるようになったものの、切り替える度にexportコマンドを実行しないといけないのは正直面倒くさいです。
もっと簡単な方法にしましょう。
ということで筆者の環境はM2Macでデフォルトのログインシェルがzshなので`.zshrc`に以下の関数を定義しました。
```zsh
switchGOROOT() {
	export GOROOT=`go$1 env GOROOT`
	export PATH=$GOROOT/bin:$PATH
	go version
}

# 重複したパスを削除
typeset -U PATH
```
引数に切り替えたいインストール済みのバージョン番号を渡してexportを実行する関数です。
これで`switchGOROOT 1.17.13`と実行するだけで良くなりました。
`# 重複したパスを削除`とコメントしてある`typeset`コマンドは、この関数(switchGOROOT)を実行する度にPATHに`$GOROOT/bin`が追加され重複していってしまうので、PATHをキレイに保つために入れています。

## バージョン番号なんだっけ問題
`switchGOROOT`関数を用意したことで切り替えは容易になりましたが、インストール済みのバージョン番号を覚えていないと素早く切り替えることはできません。
1.17といったマイナーバージョンまでは覚えていても1.17.**13**といったパッチ番号までを覚えておくことは難しいです。
そうなると`~/sdk`ディレクトリを見てインストール済みのバージョンを確認してから切り替えることになり一手間かかります。

# ツール化するぞ
というわけで更に切り替えを容易にするためにツールを作ります。
AWS Profileを切り替えるためのツールに[awsp](https://github.com/johnnyopao/awsp)というものがあるんですが、普段からよく使っていて使い心地もGOODなのでこのツールの使用感に近づけたいと思います。
awspはjsで実装されていてnpmでインストールできるのですが、今回はgoに関連することというのもあり折角なのでgoで実装したいと思います。

## govs
できあがったツールはこちらになります。
https://github.com/kane8n/govs

## os.Setenv
ツールを実装する過程で今まで知らなかった知見を得られたので共有したいと思います。

goで環境変数を設定するやり方を調べると必ず`os.Setenv`を使うやり方が出てくると思います。（というより他のやり方はないでしょう
通常、goのコードから環境変数を設定したければ`os.Setenv`を使えば良いですし、同じプロセスから設定した環境変数を参照する分には特に問題は起こりません。

環境変数を設定するツール？それなら`os.Setenv`を使えば楽勝でしょ！と思うのは筆者だけじゃないと思います。  

ですが、今回のように環境変数を設定するツールを実装したいといったケースでは実は`os.Setenv`は使えません。

どういうことか説明していきたいと思います。

### 子プロセスは親プロセスの環境に変更を加えることはできない
これよくよく考えてみれば当たり前なんですが、子プロセスは親プロセスの環境に変更を加えることは基本的にできません。
そしてシェルでプログラムを実行すると、実行されたプログラムは現在のシェルの子プロセスとして起動されます。
その子プロセスの中で環境変数を設定しても、設定されるのはあくまでも子プロセスの環境であり、終了後も親プロセスには引き継がれません。

実験していきましょう。
shellスクリプトでも同様なので実験はshellスクリプトで行います。
```bash
 $ cat sample.sh 
#!/bin/sh
# シェルの親子関係を確認するためにpsを実行しています
ps -ef | grep $1
export TEST_SAMPLE="sample"
 $ 
 $ # sample.shに渡すために現在のシェルのプロセスIDを確認します
 $ echo $$          
18598
 $ 
 $ ./sample.sh 18598
  501 18598 18548   0  9:03PM ttys013    0:00.80 /bin/zsh
  501 19834 18598   0  9:05PM ttys013    0:00.00 /bin/sh ./sample.sh 18598 # <- loginシェルの/bin/zshの子プロセスとして起動されている
  501 19836 19834   0  9:05PM ttys013    0:00.00 grep 18598
 $ 
 $ echo $TEST_SAMPLE
   # 何も出力されない
 $
```
実際に親プロセスに環境変数の追加が反映されていない様子がわかると思います。

なお`source`コマンドや`.`でこのshellスクリプトを読み込ませれば反映されます。
`source`コマンドはファイルに書かれたコマンドを現在のシェルで実行するコマンドだからです。
```bash
 $ echo $TEST_SAMPLE

 $ 
 $ source sample.sh 18598
  501 18598 18548   0  9:03PM ttys013    0:01.37 /bin/zsh
  #子プロセスの/bin/sh ./sample.sh 18598がいない
    0 21792 18598   0  9:15PM ttys013    0:00.00 ps -ef
  501 21793 18598   0  9:15PM ttys013    0:00.00 grep 18598
 $ 
 $ echo $TEST_SAMPLE     
sample
 $ 
```

というわけで現在のシェルの子プロセスとして起動されるgoのプログラムで`os.Setenv`を実行しても親プロセスである現在のシェルには反映されないということになります。

### awspではどうしているか
実はawspでもjsのプログラム上では環境変数の設定をするのではなく、`~/.awsp`というファイルに書き込む実装になっています。

そして`jsのプログラムを実行、書き込んだファイルを読み込んで環境変数をexport`という一連の処理を記述したshellスクリプトを`_awsp`というファイル名で用意し、
`alias awsp="source _awsp"`というaliasをユーザに設定してもらうことで環境変数の設定を行うという機能を実現しています。
※ `source`コマンドはファイルに書かれたコマンドを**現在のシェルで実行する**。  

実際に`_awsp`の中身を確認するとこのような実装になっています。
※ `_awsp_prompt`がnodeで実行するjsのプログラムです。
```bash
 $ which _awsp
/Users/xxxxx/.nvm/versions/node/v18.15.0/bin/_awsp
 $ cat `which _awsp` 
#!/bin/sh

AWS_PROFILE="$AWS_PROFILE" _awsp_prompt

selected_profile="$(cat ~/.awsp)"

if [ -z "$selected_profile" ]
then
  unset AWS_PROFILE
else
  export AWS_PROFILE="$selected_profile"
fi
 $ 
```
今回の実装したgovsでもawspのマネをさせてもらって機能を実現しています。

# まとめ
- goの複数versionをinstallする方法は[公式ドキュメント](https://go.dev/doc/manage-install)に従おう
- shellの関数を定義して切り替えをラクにしよう
- 更にラクにするためgoでツールを作ったら`os.Setenv`が使えないケースがあることがわかったよ
- [govs](https://github.com/kane8n/govs)をよろしくお願いします。