@eaglesakura です。
基本的にAndroid世界の片隅でコードを書いている私ですが、テスト用のサーバーをGAE/Goで構築しようかという目論見のもと、Go言語の学習もはじめました。Go言語は半年くらい前にHello World!を一度書いて以来、まったく触れてないです。
「そろそろ本格的にGoやろうかなー」みたいに考えたとき、色々障害となりそうなことを最初に排除しようとして苦労した話です。
あと、CygwinメインのWindowsユーザー以外はたぶん普通に構築できます。
IDEの選定
記憶力が低い私にとって、IDE的な機能(せめてIntelliSense、ブレークポイント、変数ウォッチ)がない環境は非常につらいです。
Android StudioのようにGoの開発環境決定版のようなものは2017Q1時点でなさそうですが、Atom, Visual Studio Code, Intellij+Goあたりが多いようです。
慣れの問題から考えて、Intellij+Goプラグインを選択しました。IntelliSense、デバッガ、ブレークポイントを始めとして、基本的な機能はかなり揃っています。
Go言語のCygwin/コマンドプロンプト問題
Go言語世界のみで、お一人様前提に構築を行なうのであれば、さほど問題はありません。
ですが、Go言語を使ったプロジェクトはGAE/Goを始めとしたサーバーサイド系が多いため、「プロジェクト」という点を考えると幾つかの問題が起こります。
例として、セットアップ等やビルド確認のためのスクリプトがあります。
私はAndroidアプリを中心にお仕事をしているので、基本的にはMac/Windows共にbashを前提にシェルスクリプトを書いておけば大きな問題は起こりませんでした。Go言語も基本的にCygwinから go get
等のコマンドが使えますが、cgo(C言語と組み合わせた)場合に問題が起こります。
cgoが含まれているとコンパイルが飛躍的に長くなり、 go install
等で事前ビルドを行なうことでビルド時間短縮が可能になります。
ですが、cygwinに含まれているgccコンパイラは一部のビルドオプションが使えず、代わりにMingwを使う場合があります。
Cygwin+Mingwを使い、 go install
して生成されたstatic libraryは、Intellij/GOプラグインからリンクすることができません。Intellijの環境が、通常のWindows環境(つまりコマンドプロンプト)を前提としているためです。
コマンドプロンプトから go install
しておけばこの問題は発生せず、Intellijでもビルドが正常に行なえます。ですが、逆にcygwinから go builid
した際に問題が発生するようになりました。
GOはライブラリ等のインストール先を GOPATH
に記述されたパスで解決します。微妙に差異がある2環境(Cygwin/プロンプト)を同居させようという点で問題が起きているようです。
そこで、Cygwinとコマンドプロンプト&IDE環境で GOPATH
を切り替えることにしました。
GOPATH の設置場所問題
Go言語の仕様上、 GOPATH
は複数ディレクトリを指すことができます。ですが、 go get
で取得できるソースコードの配置先は「最初に記述されたパス」と定められています。
通常は問題になりにくいかもしれませんが、ライブラリがバージョンアップした場合に厄介です。
あるプロジェクトは古いバージョンに依存している、新規のプロジェクトは新しいバージョンに依存している、等の状況が発生することが予想されます。
そこで、プロジェクトごとに GOPATH
を完全に切り替えるという方針を取ることにしました。
場所はプロジェクトごとに root/.gopath/cygwin
root/.gopath/windows
をそれぞれ作成して使い分けることで cgo 依存問題を回避しています。
また、リポジトリにそれらが含まれて他人に迷惑がかからないよう、 .gitignore
にそれらのパスを追加しています。
# .gitignore
/.gopath
問題点として、cygwinでフルパスを単純設定すると /cygdrive/c/path/to/project
のようなUnixライクなパス設定になってしまう(Go言語はC:\のWindows形式でパスを与えなければならない)ので、 cygpath
コマンドでの変換も必要になります。
Cygwinではこのようにすることで GOPATH
を固有に指定できます。
# Cygwinでは工夫が必要になる
# プロジェクトのrootに移動する
cd path/to/project
# 環境変数を書き換える
# GOPATHはWindows形式フルパスで存在させなければならない
# cgoを使うなら必要に応じてgccも書き換えなければならない
export GOPATH=`cygpath -w $PWD`\\.gopath\\cygwin
export CGO_ENABLED=1
export CC=x86_64-w64-mingw32-gcc
export CXX=x86_64-w64-mingw32-g++
Windowsのコマンドプロンプトではこうなります。
REM 環境変数のGOPATHをプロジェクト固有に入れ替える
SET GOPATH=%CD%\.gopath\windows
SET PATH=%MINGW64_PATH%\bin;%PATH%
GOPATH のデフォルト問題
GOPATHをプロジェクトごとに完全に切り替えてしまう
Go言語で作られたツール類は go get
で手軽に使えるという利点があります。ですが、GOPATHを完全にプロジェクト依存にしてしまうと、そのツール類をどこにインストールするか?という問題が発生してしまいました。
結局、ツール類をインストールするために環境変数 GOPATH
GOBIN
GOROOT
を設定した上で、開発プロジェクトごとに別途 GOPATH
を指定するという方向に落ち着いています。
GOPATH切替を行ってIntellijを使用する
Intellij/Goプラグインはプロジェクト固有の環境変数(GOPATH)を指定できないようです。
IDEが起動したプロセスに設定されているGOPATHを読み込み、セットアップされます。
それを利用し、次のようなバッチファイルを /root/script/windows-intellij.bat
というパスに用意することで、「現在のプロジェクトにGOPATHを指定した状態でIntellijを起動する」ということを行えるようにしています。
バッチファイルの中身はこうなります。
バッチファイルを実行するとDOS窓が居座りますが、Intellij起動後はウィンドウを閉じても問題ありません。
@echo off
REM カレントディレクトリをプロジェクトのrootにする(正確には、バッチファイルが配置されているフォルダの1階層上にする)
CD /d %~dp0\..\
REM GOPATHをプロジェクト固有に書き換える
SET GOPATH=%CD%\.gopath\windows
SET PATH=%MINGW64_PATH%\bin;%PATH%
echo GOROOT=%GOROOT%
echo GOPATH=%GOPATH%
REM Intellijを起動する
%GO_IDEA_PATH%\bin\idea64.exe
Goの依存管理ツール prjdep を作ることにした
GOPATHをCygwin/コマンドプロンプトを切り替える(1マシン・1プロジェクトに対して複数GOPATHが存在する)という都合上、既存の dep
godep
が正常に機能しませんでした。
そこで、Go言語開発の勉強を兼ねて依存解決ツール prjdep を作ることにしました。
目標は次の点です。
- Pure Golangであること
- 上記の特殊な
GOPATH
に対応可能であること(今見ているGOPATHに作用する) - 1ファイルのみで管理できること
-
go get
のみでインストール可能であること - CIを構築すること
prjdep をインストールする
go get github.com/eaglesakura/prjdep
でインストールできます。
私の環境だと次のようになります。
$GOPATH/bin
にパスを通すのを忘れないようにしましょう。
$ which prjdep
/cygdrive/c/dev-home/tools/gopath/bin/prjdep
prjdep でGOPATHの状態を保存する
prjdep init
を実行すると、「コマンドが実行された時点のGOPATHの状態を保存」します。
具体的には次のような内容の dependencies.json
がカレントディレクトリに書き出されます。
{
"Repositories": [
{
"ImportPath": "github.com/stretchr/testify",
"Rev": "4d4bfba8f1d1027c4fdbe371823030df51419987",
"Lang": "golang"
},
{
"ImportPath": "github.com/urfave/cli",
"Rev": "347a9884a87374d000eec7e6445a34487c1f4a2b",
"Lang": "golang"
}
]
}
prjdep でGOPATHの状態を復元する
prjdep restore
を実行すると、カレントディレクトリにある dependencies.json
をロードし、次のコマンドを実行します。
# 全てのライブラリに対して書きを実行する
# ライブラリをgetする
go get -d ${ImportPath}
# 保存されたリビジョンを復元する
git checkout -f ${Rev}
# ライブラリをビルドする
go install ${ImportPath}
prjdep
プロジェクト自体の依存管理も prjdep restore
で行っています。
# circle.yml抜粋
dependencies:
override:
- go version
- go get github.com/eaglesakura/prjdep
- prjdep restore
配布する上で引っかかったこと
go get
で配布するためには、ローカルpackageが使えないというルールに引っかかりました。つまり、ツール内で次のような書き方は認められていないようです。
import "./hoge/package"
また、ディレクトリ名がそのままツール名になるという go get
のルールに従うため、リポジトリ名をそのままツール名に変更しています。
【おまけ】 cgoのデバッグ実行が行えない問題の無理矢理な回避
cgo
を使ったライブラリをリンクした場合、Intellij付属のデバッガ delve が正常に起動できないという問題がありました( issue )。
フォーラム等の情報を元にすると、まずはデバッグモードで起動したい場合に -ldflags="-linkmode internal"
のビルドフラグを指定する必要があるようです。ただし、それを行った場合に上記の delve
がバイナリ解析に失敗して正常起動できなくなる問題が発生します。
そこで、Delve自体を次のように改変してビルドし、Intellij組み込みのdelveと入れ替えることでブレークポイントは働くようになります(変数のウォッチは行えなかったので、諦めてPrintfデバッグしたほうが良さそうです)。
delveはPure Golangなソリューションなので、簡単に自前ビルドを行えます。
// https://github.com/derekparker/delve/blob/master/proc/proc.go
func (dbp *Process) getGoInformation() (ver GoVersion, isextld bool, err error) {
// Debug版の場合このバージョンチェックに失敗するが、そもそもPrint以外の用途に使っていないので多分大丈夫
- vv, err := dbp.EvalPackageVariable("runtime.buildVersion", LoadConfig{true, 0, 64, 0, 0})
- if err != nil {
- err = fmt.Errorf("Could not determine version number: %v\n", err)
- return
- }
- if vv.Unreadable != nil {
- err = fmt.Errorf("Unreadable version number: %v\n", vv.Unreadable)
- return
- }
-
- ver, ok := ParseVersionString(constant.StringVal(vv.Value))
- if !ok {
- err = fmt.Errorf("Could not parse version number: %v\n", vv.Value)
- return
- }
rdr := dbp.DwarfReader()
rdr.Seek(0)
for entry, err := rdr.NextCompileUnit(); entry != nil; entry, err = rdr.NextCompileUnit() {
if err != nil {
return ver, isextld, err
}
if prod, ok := entry.Val(dwarf.AttrProducer).(string); ok && (strings.HasPrefix(prod, "GNU AS")) {
isextld = true
break
}
}
return
}
まとめ
Go言語とWindowsとCygwinとコマンドプロンプトとIntellijを悪魔合体させようとすると、開発環境構築が大変だね。
諦め is 肝心。
ドザーに幸あれ。