こんにちは、バックエンドエンジニアのKazです。
突然ですが、Go言語を使っているエンジニアのみなさまは、普段どのようにパッケージ管理を行われていますでしょうか?
今回の記事では今までの話の流れからちょっと外れて、「Go言語におけるパッケージ管理」とそれを助けてくれる素晴らしいツール「Glide」をご紹介いたします!
なお前置きだけで3000文字くらいあるので、もし用件まですっ飛ばしたい方は「Glideを使ってみよう」あたりまでスクロールしていただけると幸いです。
目次
Go言語におけるパッケージ管理の潮流
Go言語には標準で go get
という依存関係を解消する便利なコマンドが含まれていて、このコマンドを使うことでGitHubやその他あらゆるリポジトリから1コマンドで簡単にすべての依存パッケージをダウンロードし導入できるようになっています。
しかしこの go get
、実はパッケージ管理ツールとして実質必須ともいえる「バージョンの固定」ができないという悩ましい問題があります。
バージョン固定の必要性
Go言語では基本的に外部パッケージがすべて共通の「$GOPATH」下にインストールされるようになっていて、あらゆるビルドでこの配下のパッケージが参照される形になっています。
この際外部パッケージは原則としてインストールした時点で最新のものが取得され、どのようなビルドであってもこの単一バージョンのパッケージを横断的に利用する仕様になっています。
これによって常に最新のライブラリが用いられるようになり、また子孫パッケージごとに個別のバージョンがインストールされることがなくなるためビルドの高速化やバイナリの軽量化といった恩恵を受けられるようになっています。
ただしバージョンを固定できないということには致命的な問題があり、たとえば孫パッケージに破壊的な変更が加えられたときは、自動的にすべての親が互換性を失ってしまったり、それ故に開発者の意識しない箇所で新たなバグが産まれてしまったりする可能性がでてきます。
ここで、サードパーティ製でこの問題を解決するツールがいくつか出るようになりました。
パッケージのバージョン固定を可能にした「Godep」
Go言語においてパッケージの固定ができなかったのは、前述した「外部パッケージがすべて共通の$GOPATH下にインストールされ、あらゆるビルドでそれを共通利用する」という仕様によるものです。
これを解決するため、依存パッケージを共通の$GOPATHではなく個別プロジェクトのサブディレクトリにインストールして利用する方法が考案されました。これを可能にした代表的なサードパーティー製ツールのひとつに「Godep」というものがあります。
Godepは依存パッケージをサブディレクトリに展開し、パッケージの参照パスを全て書き換えることでそれらのパッケージをビルドさせることができるようになっています。
また別の動作モードとして、 go
コマンドを実行するかわりに godep go
コマンドを使ってビルドを行うことで強制的にサブディレクトリのパッケージを参照したビルドを行う機能も用意されています。
Go 1.5で導入された「Vendoring」
前述のような(なかば無理やりともいえる)方法も広く採られるようになった中で、ついにGo言語本家による解決策としてGo 1.5から「Vendoring」と呼ばれる機能が導入されました。
Vendoringの原理は前述のGodepのそれと近いものがあり、各プロジェクトのディレクトリ(またはその親ディレクトリ)に vendor
という名前のディレクトリを配置し、その中に依存パッケージを配置するとビルド時にそれらが自動的に参照される仕組みになっています。
またこの機能は公式にサポートされたものなので、Godepのように代理コマンドを使う必要はなく、ごく自然に go
コマンドを使って普段どおりにビルドするだけで各 vendor
フォルダが自動的に参照されるようになっています。
VendoringはGo 1.5から試験的に導入され環境変数で有効にすることができましたが、Go 1.6ではこれが正式導入となりVendoringが標準で有効化されたため、特になにも設定することなくVendoringの恩恵を受けることができるようになっています。
npmやComposerとの違い
このVendoringは、Node.jsにおけるパッケージマネージャーのnpmやPHPにおけるComposerなどと違い「依存関係ファイルを持たない」「依存パッケージはすべてリポジトリに含める(ことが理想とされる)」という特徴があります。
npmでは package.json
という依存関係ファイルにすべての依存パッケージの一覧とバージョンを定義し、かつダウンロードされたパッケージを含む node_modules
ディレクトリ(Vendoringにおける vendor
ディレクトリに相当)は .gitignore
によってリポジトリの管理から外してしまうといった運用が一般的に行われています。
Go言語におけるVendoringは言ってしまえばこれとは真逆のアプローチで、「そもそも依存ファイルの存在を外部リポジトリに依存するのではなく、すべてのファイルを自身の責任において管理したほうがよい」という思想に基づきこのような形が採用されています。
依存パッケージの一覧とバージョンの定義
このVendoringによって依存パッケージのバージョン管理はサードパーティ製のツールを用いることなく実現することができるようになりました。が、依然パッケージ管理としては不便な点も残ります。冒頭で紹介したようにGo言語では go get
コマンドを使って依存パッケージを簡単にダウンロードすることができるのですが、このコマンドはバージョン固定のために毎回 vendor
フォルダ下への配置を行ってくれないのです。
これを解消する方法として、Vendoringに対応しつつ簡単に依存パッケージを取得してくる方法として近年様々なサードパーティ製のツールが登場してきています。もちろん前述の、長らく代表格の座を保ってきた「Godep」でもVendoringへの対応が行われ、Godepのバージョン管理機能は保ったまま、自動的にパッケージを vendor
ディレクトリ下に展開するようになりました。
さてすっかり前置きが長くなりましたが、今回の記事ではこのVendoringに対応したパッケージマネージャーとして近年注目されている「Glide」というツールを紹介していきます。
Go言語におけるシンプルかつ強力なパッケージ管理の最適解「Glide」
LIGでは現在GoのパッケージマネージャーとしてGlideを採用しているのですが、この理由として下記のメリットがありました。
1. YAMLで書ける
Glideは、依存関係の定義ファイルをYAMLで記述します。この利点ときたら他の何物にも代えがたいものがあります。
他のパッケージマネージャーにおける定義ファイルの記述フォーマットを見ると、GodepなどではJSONが採用されており、ちょっと変わったところだとRubyのGemfileにインスパイアされたGomの「Gomfile」フォーマットなどもありました。
もちろんJSONは最高のフォーマットですし、Gomfileも便利な機能の恩恵を受けられるのですが、選定の肝となったのはシンプルで強力なYAML記法の手軽さでした。
2. 必要最低限の機能がシンプルにまとまっている
Glideは、必要最低限の機能だけをごくシンプルに提供しています。たとえばVendoring環境下でテストを実行する際はvendorディレクトリを除外しないと毎回膨大なテストを走らせることになってしまうのですが、この解決のために他のライブラリたちがこぞって独自コマンドを提供しテストコマンドをラップさせるなどわりと仰々しいアプローチをとっているのに対し、Glideではシンプルに「vendor以外のディレクトリの一覧を返す」だけのコマンドを実装しています。
このようにシンプルな機能と分かりやすいYAMLだけで必要な機能を全て実現できるGlideが、現状ではベストな選択肢であると判断しました。
Glideを使ってみよう
Glideのインストール
Glideのインストールはとても簡単です。
LinuxやMacへのインストールは、下記コマンド一発で完了します。
curl https://glide.sh/get | sh
初っ端からたったこれだけ。とてもシンプルですね。
Macへのインストール: Homebrew編
(上記curlでのインストールを行った方はこのステップは不要です)
Homebrewを使っている方は、上記のcurlによる方法のほかBrewによるインストールも可能です。
brew update
brew install glide
Ubuntuへのインストール: apt-get編
Debian/Ubuntuをお使いの方はAPTを使ったインストールも可能です。
sudo add-apt-repository ppa:masterminds/glide
sudo apt-get update
sudo apt-get install glide
ちなみにLIGではCircleCIを使って継続的デプロイメントを行っているのですが、CircleCIの実行環境(Dockerコンテナによる仮想環境)はUbuntuなのでAPTを使ってGlideの導入を行っています。
バージョン定義ファイルの生成
Glideをインストールできたら、次のコマンドで初期のバージョン定義ファイルを生成します。
glide init
このコマンドを実行するとサブディレクトリを含めた全てのパッケージが走査され、インポートしている外部ライブラリの一覧が glide.yaml
というバージョン定義ファイルに書きだされます。
依存パッケージの取得と更新
バージョン定義ファイルができあがったら、次のコマンドを実行して必要なパッケージをまるごとダウンロードしてインストールすることができます。
glide install
なおこのコマンドを実行すると /vendor
ディレクトリが作成され依存パッケージがそこに全てダウンロード・展開されるのですが、それと同時に同じディレクトリに glide.lock
というファイルも生成されます。このファイルは現在展開されているパッケージのバージョンがすべて羅列された、いわばバージョンのロックファイルになります。
一旦このロックファイルができたあとは、いつ何度 glide install
コマンドを叩いても、常に同じバージョンのパッケージがダウンロードして展開されるようになります。
ネストしたVendorディレクトリの削除
インポートしてきたパッケージがさらにその中にVendorディレクトリを持っていた場合、Vendorディレクトリがネストしてしまうことがあります。
/vendor/github.com/lorem/ipsum
/vendor/github.com/foo/bar
/vendor/github.com/foo/bar/vendor/github.com/lorem/ipsum
この場合Go言語ではネストされたVendorディレクトリ下のパッケージをそれぞれ独立したものとして扱うため、上記の例では /vendor/github.com/lorem/ipsum
と /vendor/github.com/foo/bar/vendor/github.com/lorem/ipsum
はそれぞれ別物として認識されてしまいます。
この場合もちろんビルド時間は倍になってしまいますし、コンパイル後のバイナリファイルもその分無駄に肥大化してしまいます。それどころか本来同じはずのパッケージが別物として扱われることから、コードによっては型の不一致でコンパイルエラーを引き起こしてしまうこともあります。
これを防ぐため、GlideではネストしたVendorディレクトリを削除して単一階層のVendorディレクトリに取りまとめる機能を提供しています。この機能は glide install
コマンドに -v
オプションを付けることで利用できます。
glide install -v
この機能は地味に重要で、特に型の不一致によるエラーなどに悩まされた際には必須となるコマンドです。
依存パッケージの更新
依存パッケージはパッチアップデートなどが行われることがあるため、定期的にアップデートを行う必要があります。その際は次のコマンドを叩くだけでパッケージの一括アップデートを行えます。
glide up
このコマンドは後述する glide.yaml
においてバージョンが指定されていないパッケージを、可能な限り新しいバージョンまでアップデートしてくれます。アップデート後は自動的に glide.lock
も更新されます。
バージョンの完全固定
glide.lock
ファイルでバージョンを定義しただけだと glide update
コマンドでバージョンが上書きされてしまいます。しかし意図的にバージョンを固定しておきたいケースももちろんありえるため、その場合は glide.yaml
ファイルを編集してバージョンの指定を行います。
import:
- package: https://github.com/labstack/echo
version: v2.0.2
ここではコミットID、ブランチ名、タグをバージョンとして指定できます。またタグの命名規則がセマンティック・バージョニングに対応していれば、対応バージョンのレンジ指定を行うこともできます。
パッケージの追加導入
開発を進めていく上でパッケージを追加導入する必要がでてきたら、次のコマンドを叩きましょう。
glide get golang.org/x/net/websocket
これだけで自動的にパッケージのダウンロードとVendorディレクトリ下への展開、glide.yaml
への追加定義および glide.lock
の更新を行ってくれます。
またこのコマンドではバージョンの指定も行えます。
glide get github.com/gin-gonic/gin#v1.0rc1
ここで指定したバージョンはもちろん glide.yaml
にも反映されるため、 glide update
などで不意に更新されることなく永続的にそのバージョンが参照されるようになります。
glide get
の実行には少し時間がかかりますが、この間自動的に全てのサブパッケージを走査し必要な依存関係の解消と最適化を行ってくれているようです。
ユニットテストの実行
パッケージ全体に渡ってユニットテストを行うとき、下記のようにテストを実行するとVendorディレクトリ下のパッケージまで一律にテストが呼び出されてしまいます。
go test ./...
これを防ぐため、GlideではVendor以外のディレクトリを列挙する glide nv
というコマンドを用意しています。
go test $(glide nv)
これによってVendorディレクトリ以外の全てを対象としたユニットテストを行えます。
Glide、おすすめです
さて、いかがでしたか? 弊社で活用を始めたGlide、使い勝手が良くてなかなかおすすめです。Go言語を使い始めたけれどまだパッケージのバージョン固定をしていないという方、今使っているツールがイマイチという方はぜひお試しください。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。