goenvとdirenvを使ってGAE/goの環境を見直す

※ 2017年10月に、anyenvで使用するgoenvは、syndbg/goenvに変更されています。


## はじめに
goでGAEのプログラムを書くのって楽しいですよね。go, GAEに魅了されてしまった、saizzです。

最初は、右も左もわからず、とりあえず開発環境を作っていたのですが、今後、GAE/goのgoのバージョンが1.8に上がった時を考え、環境を見直しました。OSは、Ubuntuです。

### やりたいこと

- appengine-go-sdkのバージョンをプロジェクト（のディレクトリ）毎にすみ分けできるようにする

### 現在の状況

```sh
/home/saizo/google/
├── go
├── gopath
├── appengine -> ./google-cloud-sdk/platform/google_appengine
└── google-cloud-sdk
```

こんな感じで、`~/google`以下

- goディレクトリにgoのsdk
- gopathディレクトリにプロジェクト関係なくバイナリだけ利用するようなものをget getするgopath
- gcloudでappengine-go-sdkがあることに目新しさを感じ、gcloudでappengine-go-sdkを導入

としていました。
（GAE/goだけなら、goのsdkは必須ではないですよね。最初は知らなかった）

`gcloud components update`でappengine-go-sdkをアップデートできるお手軽さはあるのですが、すみ分けはできないです。

## goenvというものがあるらしい

qiitaでもいくつか記事が見つかります。更に調べてみると、いろんな人がgoenvを作っているのがわかります。
一部紹介すると、

- https://bitbucket.org/ymotongpoo/goenv
 
`goenv -gae`でGAEの`app.yaml`のファイルも作ってくれる。appengine-go-sdkは別途用意しなければいけなさそう

- https://github.com/kaneshin/goenv

`goenv install gae-x.x.x`でappengine-go-sdkをダウンロードでき、goと同じように管理できる

今回、ダウンロードに対応している、kaneshin/goenvを使うことにしました。

## anyenvというものがあるらしい

更に_all in one for **env_という、色々な言語のバージョン切り替えツールをラップして設定の煩雑さを吸収したツールがあります。

https://github.com/riywo/anyenv

goenvにも対応しており、kaneshin/goenvを使うようになっています。

## anyenvでkaneshin/goenvを導入する

導入は簡単です。Ubuntuに導入したので、your_profileは`.bashrc`です。

```shell-session
$ git clone https://github.com/riywo/anyenv ~/.anyenv
$ echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.your_profile
$ echo 'eval "$(anyenv init -)"' >> ~/.your_profile
$ exec $SHELL -l
$ anyenv install goenv
$ exec $SHELL -l
```

そして、導入可能な最新のgoを通常使うようにして、追加でgae-1.9.53を導入します。

```shell-session
$ goenv install 1.8.3
$ goenv global 1.8.3
$ goenv install gae-1.9.53

```

GAE/goのプロジェクトでは、gae-1.9.53を使用するよう指定します。

```shell-session
$ cd ~/workspace/gae-project
$ goenv local gae-1.9.53
```
カレントディレクトリに`.go-version`というファイルが作られ、バージョン情報が書き込まれています。

これで、`~/workspace/gae-project`配下のどこでもよいので、

```shell-session
$ goenv rehash
```

で、gae-1.9.53が使えるようになります。

また、上記以外のディレクトリで`goenv rehash`すると、globalが適用されて、go-1.8.3が使えるようになります。

## ちょっと困った

goenvの仕組みは、以下のようです（他のgoenvも同じっぽそうです）。

- `~/.anyenv/envs/goenv/versions`に各バージョンのgo, appengine-go-sdkが格納される
- `goenv rehash`で`~/.anyenv/envs/goenv/shims`に指定バージョンのファイルがコピーされる
- pathは、`~/.anyenv/envs/goenv/shims`に通っており、結果的に切り替えられる

あるシェル上で`goenv rehash`すると、他のシェルでも影響がでそうです。`goenv shell`でそのシェルでのみ環境を切り替えられそうですが、この場合、gae-projectのディレクトリに結びつけはできなさそうです。

また、gae-1.9.53を使うようにしていると、goコマンドはpath上に存在しなくなります。

私は、エディタにemacs（細かくいうとspacemacs）を使っています。非常に強力ですが、go-modeの時にgoコマンドを要求してきます。

そこで、goenvはsdkのダウンロードマネージと、globalの指定のみとして、ディレクトリ毎で環境を切り替えるのは別の手段を取りました[^1]。

[^1]: appengine-go-sdk内のgoappにシンボリックリンクでgoを作ってみたけど、そのgoは起動時にエラーになってしまい、goとappengine-go-sdkの両方をpath指定するように。

## direnvというものがあるらしい

https://github.com/direnv/direnv

bash, zsh, tcsh, fishに対応しており、ディレクトリの変更をhookして、環境をload, unloadする仕組みです。

Ubuntuのパッケージが用意されているので、インストールはapt一発です。

そして、hookするように`.bashrc`に以下を追加します。

```shell-session
eval "$(direnv hook bash)"
```

emacsが要求する（笑）ので、1.6系のgoをダウンロード

```shell-session
$ goenv install 1.6.4
```

`~/.direnvrc`を作成し、kaneshin/goenvで導入されているバージョンから、指定のバージョンの環境をpathに追加するスクリプトを書く。

```shell-session
# ~/.direnvrc
use_go() {
  local version=$1
  local found=0
  while read line; do
    name=${line}
    if [ "${name}" = "${version}" ]; then
      export GOROOT="${GOENV_ROOT}/versions/${name}"
      path="${GOENV_ROOT}/versions/${name}/bin"
      PATH_add ${path}
      found=1
      break
    fi
  done < <(goenv version-names | grep -v gae)

  if [ "$found" -eq 0 ]; then
    echo "go '${version}' is not installed." >&2
    return 1
  fi
  return 0
}

use_gae() {
  local version=$1
  local found=0
  while read line; do
    name=$(echo ${line} | cut -d '-' -f 2)
    if [ "${name}" = "${version}" ]; then
      path="${GOENV_ROOT}/versions/gae-${name}"
      PATH_add ${path}
      found=1
      break
    fi
  done < <(goenv version-names | grep gae)

  if [ "$found" -eq 0 ]; then
    echo "gae '${version}' is not installed." >&2
    return 1
  fi
  return 0
}
```
(追記) use_goの方に、GOROOTの環境変数の設定を追加しました。

`~/workspace/gae-project/.envrc`を作成し、hook時に行う処理を書く

```shell-session
# ~/workspace/gae-project/.envrc
use gae 1.9.53
use go 1.6.4
export GOPATH=$(pwd)
```

direnvは、bash以外にもzsh, tcsh, fishに対応していますが、`.direnvrc`, `.envrc`はbashで実行されるとのこと。
bashの文法で記述する必要があります。

また、.envrcに`layout go`と記載すると、その時点のgopathの先頭にカレントディレクトリを追加などしてくれます。
なので、gopathをカレントディレクトリだけにしたい場合、

```shell-session
unset GOPATH
layout go
```
としないといけません。わかりづらいので、layoutは使わず、`export GOPATH=$(pwd)`としています。

作成したら、設定を反映します。
```shell-session
$ direnv allow .
```

もう少しシンプルな仕組みにしたかったけど、一先ず、やりたいことはできた。

## 最後に

やってみたけど、よくよく考えたら、deployする先のappengineはどんどんバージョンが上がっていくわけだから、プロジェクト毎にappengine-go-sdkのバージョンを固定する意味ってあまりないか。。バージョンの切り替えが簡単にできるってぐらいですかね。

## 大きく見直し（2018年10月追記）

GAE/goの場合、appengineのパッケージもdepなどのパッケージ管理ツールを使い、プロジェクト内のパッケージ配下のvendorディレクトリに含めてしまうことが多いです。

そうなると、~/.direnvrcに独自にスクリプト書いて切り替える必要はなく、

* goenvで使用するgoのバージョンを切り替え
* プロジェクトの.envrcは
    * 環境変数GOPATHをプロジェクトルートに切り替える
    * 環境変数GOROOTを参照するエディタ用に環境変数GOROOTを明示的に設定する

これだけになりました。
具体的には、

```.envrc
export GOROOT=$(go env GOROOT) 
export GOPATH=$(pwd)
```

とだけになりました。