Goのディレクトリレイアウト、IntelliJ IDEAの設定、バージョン管理を考える


この記事は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なソースの中身を実装してみます。

yammy/tasting.go
package tasting

import "github.com/pkg/errors"

func Tasting(object string) error {
    if object == "cookie" {
        return nil
    }
    return errors.New("not yammy")
}
main.go
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"としなければならなくなるでしょう。

yammy/tasting.go
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というファイルを作って編集します。

.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!

ホワイトプラスではエンジニアを募集しています

ホワイトプラスでは、新しい技術にどんどん挑戦したい!という技術で事業に貢献したいエンジニアを募集しております。!