この記事はWHITEPLUS Advent Calendar 2016 12日目になります。
カプセルホテル巡りが趣味の Go言語エンジニア kai-zoa です。
掲題の件についてはどうしたらいいんですかね。
自分なりに答えを出してみます。
#その後(2017-01-29)
この記事は当時どうにかプロジェクト単位でGOPATH
を設定できないか試行錯誤していたときの備忘録ですが、もし同じことを考えてる人がいたらはっきり言ってそれは諦めることをオススメします。
なぜならプロジェクト単位でGOPATH
を変えたり後述の方法でやっているような無茶をするとGoのエコシステムによる様々な恩恵が得られません。
なのでGOPATH
はシステム共通でひとつのパスを決めましょう。
Go1.8からは環境変数にGOPATH
を設定しなくても、$HOME/go
がデフォルトのGOPATH
として扱われるので何も考えずに$HOME/go
の下で作業するべきだと思います。
つまり下記からは読む必要がありません。完
#要件
- 複数バージョンのgoを管理する環境が前提
-> anyenvで入れたgoenvからをGoインストール - 依存ライブラリのバージョン管理ができる
-> glideをインストール - GOPATHに含めるのは開発中のコードと依存ライブラリのみにする
- Intellij IDEAでの開発をサポートする
goenvの導入とお好きなバージョンのGoのインストール(ただし1.5.1以上)、glideの導入は事前に行って下さい。
https://github.com/riywo/anyenv
https://github.com/Masterminds/glide
プロジェクトを作成し、コードを書いてみる
要件はシンプルですが、問題になるのはプロジェクトディレクトリのレイアウトです。
##プロジェクトの作成
例としてpkg/errors
に依存する、一般的なディレクトリレイアウトのプロジェクトを作ってみます。
# goenvの環境を環境変数にセットする
# (後述しますが、この手順には問題があります)
$ export GOROOT=${HOME}/.anyenv/envs/goenv/versions/`goenv version`
$ export GOPATH=${GOROOT}
$ export PATH=${PATH}:${GOROOT}/bin
# プロジェクトディレクトリを作成して、Gitリポジトリとして初期化
$ mkdir yammy
$ cd yammy
$ git init
# yammyなソースを作る
$ echo package main > main.go
$ mkdir tasting
$ echo package tasting > tasting/tasting.go
# pkg/errorsをglideでインストール
$ glide get github.com/pkg/errors
# コミット除外ルール
$ echo /.idea > .gitignore
$ echo /vendor >> .gitignore
# 初期コミット
$ git add .
$ git commit -m 'initial commit'
ディレクトリレイアウトはこのようになります。
$ tree
.
├── .git
├── .gitignore
├── glide.lock
├── glide.yaml
├── main.go
├── vendor
│ └── github.com
│ └── pkg
│ └── errors
│ │
│ ...
└── tasting
└── tasting.go
めでたくglideでのバージョン管理下での依存パッケージインストールもできましたが、このままだとGOPATHの設定を適当にGoのインストールディレクトリにしているために問題が起こります。
##実装
yammyなソースの中身を実装してみます。
package tasting
import "github.com/pkg/errors"
func Tasting(object string) error {
if object == "cookie" {
return nil
}
return errors.New("not yammy")
}
package main
import (
"tasting"
"fmt"
)
func main() {
objects := []string{"pen", "cookie"}
for _, o := range objects {
fmt.Printf("%s: ", o)
if err := tasting.Tasting(o); err != nil {
fmt.Print(err)
} else {
fmt.Print("yammy")
}
fmt.Println()
}
}
実行 〜 そして問題を発見する
コードには問題はありませんが、実行してみると問題に気づきます。
$ go run main.go
main.go:4:2: cannot find package "yammy" in any of:
/Users/kai.zoa/.anyenv/envs/goenv/versions/1.7.3/src/yammy (from $GOROOT)
/Users/kai.zoa/.anyenv/envs/goenv/versions/1.7.3/src/yammy (from $GOPATH)
GOPATHからyammyなパッケージを探せないためエラーとなります。
それではということで、プロジェクトのディレクトリをGOPATHにしてみますが…
$ export GOPATH=`pwd`
$ go run main.go
main.go:4:2: cannot find package "tasting" in any of:
/Users/kai.zoa/.anyenv/envs/goenv/versions/1.7.3/src/tasting (from $GOROOT)
/Users/kai.zoa/Codes/yammy/src/tasting (from $GOPATH)
まだだめです。これはGoが${GOPATH}/src
にソースコードがあることを期待するためです。
#問題を改善する
まず、GOPATHに期待されるディレクトリレイアウトはHow to Write Go Codeにある通りで、Vendoringを利用する今回の場合は下記のようになっているのが正しいです。
${GOPATH}
└── src
└── yammy
├── glide.lock
├── glide.yaml
├── main.go
├── yammy
│ └── tasting.go
└── vendor
└── github.com
└── pkg
└── errors
│
...
ということで、Gitリポジトリの直下にsrc/
を作ってコードを移動すればリポジトリ直下をGOPATHにできると考えてしまうかもしれませんが…
yammy
└── .git
└── src
└── yammy
└── tasting.go
...?
実はGitリポジトリ下のレイアウトとしては現状のままで良くて、わざわざsrc/
を作ってレイアウトを変える必要はありません。
これはインストールしたpkg/errorsを参考にして見ればわかるのですが、標準的にはリポジトリ直下にsrc/
を置きません。
github.com
└── pkg
└── errors
├── LICENSE
├── README.md
├── appveyor.yml
├── bench_test.go
├── errors.go
...
もしそれをやってしまうと、次にglideやgo-get経由でインストールしたときにyammyでのimport文はimport "github.com/pkg/errors/src"
としなければならなくなるでしょう。
import "github.com/pkg/errors/src"
func Tasting(object string) error {
if object == "cookie" {
return nil
}
return src.New("not yammy") // なんじゃこりゃ?????
}
次に考えるのはあらかじめ全てのGo言語開発のワークスペースのルートとなるGOPATH/srcディレクトリを作成しておくとか、Goのインストールディレクトリのsrcのなかで作業するとかだと思います。
しかし、この方法だと関係ないプロジェクトのモジュールもGOPATHに見えてて共有されてしまうため、想定外の問題に遭遇する未来しか見えないので、自分はやってません。
そして最後に残された道はプロジェクトごとにGOPATH/srcを作ることです。
しかしながら、Gitリポジトリをクローンする際に毎回GOPATH/srcを各開発者が作らなければいけません。
なんかそれはダサいですね。どうにかならないんでしょうか‥
##direnvでプロジェクトごとにGOPATHを作る
ホワイトプラスでは開発者とデザイナーのマシンに必ずdirenvをインストールしてもらっています。
今回はこいつを使ってプロジェクトごとのGOPATHを自動で作ることにしました。
###direnv is 何?
direnvはシェル上で自動で環境の切り替えを行うツールです。
プロジェクトのルートディレクトリにdirenv用のスクリプトを置いておけば、cdしたときに自動で実行してくれます。
他のディレクトリへ移動した際には自動でスクリプト内で設定した環境変数をもとに戻してくれます。
###GOPATHを自動作成するdirenvスクリプト
プロジェクトのルートに.envrcというファイルを作って編集します。
#!/usr/bin/env bash
export PKG=github.com/kai-zoa/yammy
export GOPATH=`pwd`/.__GOPATH__
PATH_add ${GOPATH}/bin
[ ! -d ${GOPATH} ] \
&& ( \
mkdir -p ${GOPATH}/src && \
mkdir -p ${GOPATH}/pkg && \
mkdir -p ${GOPATH}/bin && \
mkdir -p `dirname ${GOPATH}/src/${PKG}` && \
ln -fns ../`echo ${PKG} | sed -e 's/[^\/\]*/../g'` ${GOPATH}/src/${PKG} \
|| ( \
echo failed to create GOPATH layout 1>&2 ; \
exit 1 \
) \
)
あらかじめ変数PKGには公開時のパッケージ名をセットして、スクリプトを実行します。
$ direnv allow
direnv: loading .envrc
direnv: export +PKG ~GOPATH ~PATH
$ env | grep '^GOPATH'
GOPATH=/Users/kai.zoa/Codes/yammy/.__GOPATH__
やっていること
- GOPATH(src, bin, pkg)のディレクトリレイアウトを作成する
- プロジェクトのディレクトリをパッケージ名のリンクとして作成する
- GOPATH内で実行したことにするgoコマンドのラッパーを${GOPATH}/binに作成する
※ goコマンドのラッパーがないとテスト実行時にライブラリのパスが通らないため
$ tree
.
├── .__GOPATH__
│ ├── bin
│ │ └── go
│ ├── pkg
│ └── src
│ └── github.com
│ └── kai-zoa
│ └── yammy -> ../../../..
├── .envrc
├── .gitignore
├── glide.lock
├── glide.yaml
├── main.go
├── tasting
│ └── tasting.go
└── vendor
└── github.com
└── pkg
└── errors
...
GOPATHが正しいレイアウトになっているので、実行にも成功します。
$ go run main.go
pen: not yammy
cookie: yammy
#Intellij IDEAの設定
ここまで環境構築を終えてればIntellij IDEAではGoプロジェクトとして開いて、
Languages & Frameworks -> Project libraries
に自動作成されたGOPATHディレクトリを追加するだけでOKです。
自分はVimmerですが、Intellij IDEAでの開発をサポートしておくとGoの敷居もぐっと下がり大変よろしいことと思います。
#gopkg.in 〜 もうひとつのGoライブラリのバージョン管理について
今回依存ライブラリのバージョン管理には最近人気がありそうなglideを使いました。こうした他プログラミング言語でもよくみる依存解決用のファイルを使う方法以外にGoらしいバージョン管理方法として公開パッケージの名前にバージョン番号を含める方法があります。
gopkg.inではこの方法をサポートしていて、Gitのタグでsemver対応していればパッケージ名でのバージョン指定からGitHub上のソースへのリダイレクトを行ってくれます。
$ go get gopkg.in/yaml.v1
この方法だとサポートはライブラリの開発者に委ねられますが、こっちの方がGoらしいので好きです。
といっても全てのライブラリが対応してるとも限らないのでまだまだglide系のツールに頼る必要がありますが‥
#明日
デザイナーのnimoni373!
#ホワイトプラスではエンジニアを募集しています
ホワイトプラスでは、新しい技術にどんどん挑戦したい!という技術で事業に貢献したいエンジニアを募集しております。!