この記事はZOZO AdventCalender 2023シリーズ4の6日目の記事です。
この記事の内容は古い情報です。
Go1.21から導入されたGOTOOLCHAINを使うことで、複数バージョンのGo言語をインストールする必要がなくなりました。
始めに
go言語で複数バージョンをPCにインストールして切り替える方法について説明します。
なおgo言語ではGo1.x系の間には後方互換性を破壊するような変更は加えないということがルール化されているので、
基本的には最新バージョンを使用すれば良く、複数バージョンをPCに入れる必要はありません。
後方互換性を破壊するような変更を加えないといけないといった状況になった際にはGo2としてリリースされるという方針になっているようです。
しかしながら諸般の事情で複数バージョンをPCに入れて切り替えたいということはありえます。
筆者も最新バージョンではビルドができないというプロジェクトに最近関わることになり、最新バージョンとは別に1.17系を入れる必要が出てきて複数バージョンを扱う方法を調べました。
この記事ではそこで得た知見を共有したいと思います。
複数バージョンをインストールする方法
一昔前までは他の言語でもあるようなxxenvというような名前のツールでgoenvが人気があった印象ですが、現在では開発が止まっているようであまりオススメできません。
しかしながら実は公式ドキュメントで複数バージョンをインストールする方法について説明しているものがあります。
公式が安定なのは間違いないのでここは素直に公式ドキュメントで説明されてる方法で複数バージョンのインストールをすることにします。
install
1.17.13をインストールしたい場合、以下のコマンドを実行します。
※他のバージョンを入れたい場合はバージョン番号部分(1.17.13)を入れたいバージョンのものにすれば良いです。
$ go install golang.org/dl/go1.17.13@latest
$ go1.17.13 download
このコマンドを実行すると~/sdk
以下に指定したバージョンのgoがインストールされます。
$ ls -l ~/sdk
total 0
drwxr-xr-x 21 xxxxx staff 672 11 2 15:37 go1.17.13
$
インストール可能なバージョンはdownload pageで確認することができます。
切り替え
公式のやり方でインストールしたgoコマンドは、goコマンドの末尾にインストールしたバージョン番号を追加したコマンドで実行することになります。
$ go1.17.13 version
go version go1.17.13 darwin/arm64
$
なるほど確かにこれなら複数バージョンを扱えますね!と言いたいところですが、
プロジェクトでビルドスクリプトやMakefileを用意している場合、goコマンドの末尾にバージョン番号を追加していたりはしないので正直使いづらいです。
goコマンド自体を切り替えることができれば使いやすくなりそうなので、やり方を考えていきます。
goコマンドを差し替える
GOROOT
という環境変数があるのでこれを切り替えればOKです。
昔は公式ドキュメントにGOROOT
についての記述があった気がするのですが、現在の公式ドキュメントではソースインストールのドキュメントで触れられてるのみでした。
この環境変数はGo SDKのpathを設定するものなので、複数バージョンを扱うといったユースケースで使うものになり、まさに今回のケースで使うための環境変数です。
切り替えは以下のようにやります。
$ 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
に以下の関数を定義しました。
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というものがあるんですが、普段からよく使っていて使い心地も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スクリプトで行います。
$ 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
コマンドはファイルに書かれたコマンドを現在のシェルで実行するコマンドだからです。
$ 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のプログラムです。
$ 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のマネをさせてもらって機能を実現しています。