Lucas F. Costa Human thoughts about exact sciences
皆さん、こんにちは。 お気づきかもしれませんが、私は Go にとても興味があり、この言語が好きになったので、もっと頻繁にそれについて書きたいと思います。 Go は素晴らしいです!
Go は比較的若い言語で、ベンダリングと依存関係の管理に関しては、まだ本当に成熟していません。 しかし、Go コミュニティは、この需要を満たすために、いくつかのプラクティスを採用し、ツールを作成しています。
Go エコシステムの理解
Go は、複雑なタスクを達成するためにシンプルさを使用し、したがって、言語をより楽しく、生産的にすることを目的としています。 言語の初期から、Go チームは文字列リテラルを使用して、インポートされるものを説明するためにインポート構文を任意にすることを選択しました。
これは、彼ら自身のドキュメントに書かれていることです。 もし Go がプログラムのビルド方法を説明するために設定ファイルを必要とするなら、Go は失敗していたでしょう。
この目標を達成するために、Go チームは設定よりも規約を優先しました。 パッケージ管理に関して採用されている規約は次のとおりです。
- import パスは常に、ソース コードの URL から既知の方法で導き出されます。 これが、インポートが次のようになる理由です。
github.com/author/pkgname
. このようなオンライン・サービスはすでに各パッケージに固有のパスを管理しているため、こうすることで自動的に管理されたネームスペースも得られます。 - ファイルシステム内のパッケージを格納する場所は、インポートパスから既知の方法で導き出されます。
go
がダウンロードしたパッケージをどこに格納するかを検索すれば、それがダウンロードされた URL に関連付けることができます。 - ソースツリーの各ディレクトリは、単一のパッケージに対応しています。 これにより、特定のパッケージのソースコードを見つけやすくなり、標準的な方法でコードを整理することができます。 フォルダ構造をパッケージ構造に結びつけることで、ファイルシステムツールがパッケージ管理ツールになるので、両方を同時に心配する必要はありません。
- パッケージはソースコードからの情報のみを使って構築されます。 これは
makefiles
やconfiguration
がないことを意味し、特定の (そしておそらく複雑な) ツールチェインを採用することから解放されます。
さて、ここまででなぜ go get
コマンドがそのように動作するのかを理解するのは簡単でしょう。
Go Tools のコマンドを理解する
これらのコマンドに入る前に、「$GOPATH
」とは何かを理解しましょう。
$GOPATH
は、ワークスペース ディレクトリと考えられるディレクトリを指し示す環境変数です。
go get
go get
コマンドはシンプルで、ほとんど git clone
と同じような動作をします。 go get
に渡さなければならない引数は、単純なリポジトリの URL です。 この例では、コマンドを使用します。 go get https://github.com/golang/oauth2
.
このコマンドを実行すると、go
は単に指定された URL からパッケージを取得し、$GOPATH
ディレクトリに配置します。 $GOPATH
フォルダに移動すると、パッケージのソースファイルを含む src/github.com/golang/oauth2
フォルダと、コンパイルされたパッケージが pkg
ディレクトリに (依存関係とともに) 存在していることが確認できます。
go get
を実行するときはいつでも、ダウンロードしたパッケージはダウンロードに使用した URL に対応するディレクトリに置かれることを念頭に置いてください。
また、go help gopath
によると、go get
コマンドは取得するパッケージのサブモジュールを更新することもできるようです。
go install
パッケージのソースディレクトリで go install
を実行すると、そのパッケージの最新バージョンと pkg
ディレクトリのすべての依存関係をコンパイルします。
go build
go build はパッケージとその依存関係のコンパイルを担当しますが、 結果をインストールはしません !
Understanding Vendoring
Go が依存関係を保存する方法によって気づいたかもしれませんが、依存関係管理へのこのアプローチにはいくつかの問題があります。 これは、もし誰かが自分のパッケージに対して破壊的な変更を行い、それを別のリポジトリに置かなかった場合、あなたとあなたのチームは、同じパッケージの異なるバージョンを取得することになり、「私のマシンでは動作する」という問題につながるので、結局困ったことになるということを意味します。 つまり、同じパッケージの異なるバージョンに依存するプロジェクトを持つことはできず、一度にどちらかのバージョンを持つ必要があります。
これらの問題を軽減するために、Go 1.5 からは、Go チームは vendoring
機能を導入しました。 この機能により、パッケージの依存関係のコードを独自のディレクトリに置くことができ、すべてのビルドで常に同じバージョンを取得できるようになります。
例えば、awesome-project
というプロジェクトがあり、それが popular-package
に依存しているとします。 チームの全員が popular-package
の同じバージョンを使用することを保証するために、そのソースを $GOPATH/src/awesome-project/vendor/popular-package
の中に置くことができます。 これにより、Go
は $GOPATH/src
ではなく、自身のフォルダの vendor
ディレクトリ (少なくとも 1 つの .go
ファイルがある場合) から依存関係のパスを解決しようとするため、うまくいきます。 これにより、ビルドは常に同じバージョンの popular-package
.
を使用するため、決定論的 (再現可能) になります。また、go get
コマンドは、ダウンロードしたパッケージを自動的に vendor
フォルダに配置しないことに注意する必要があります。 これは vendoring ツールの仕事です。
Vendoring を使用している場合、Go
は常に最も近い vendor
ディレクトリに依存関係を見つけようとするので、そうでない場合と同じ import パスを使用することが可能です。 インポートパスの前に vendor/
を付ける必要はありません。
ベンダリングがどのように機能するかを完全に理解できるようになるには、インポート パスを解決するために Go が使用するアルゴリズムを理解する必要があります。
- Look for the import at the local
vendor
directory (if any) - If we find this package in the local
vendor
directory we go up to the parent folder and try it in thevendor
directory there (if - $GOPATH/src に到達するまで手順 2 を繰り返します
- $GOROOT でインポートしたパッケージを探します
- $GOROOT でパッケージを見つけられない場合は $GOPATH/src フォルダーで探します
基本的には、以下のとおりです。 これは、各パッケージが独自の依存関係を独自のベンダーディレクトリに解決できることを意味します。 たとえば、パッケージ x
とパッケージ y
に依存していて、パッケージ x
もパッケージ y
に依存しているが、異なるバージョンのパッケージが必要な場合、x
は自身の vendor
フォルダで y
を探し、パッケージはプロジェクトの vendor フォルダで y
を探すため、問題なくコードを実行することが可能です。 たとえば、次のようなフォルダ構造だとします。
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
もし、github.com/user/package-one
を main.Xrd の内部からインポートすると、
のようになります。
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go vendor/ github.com/user/package-one/ one.go
ここで、同じパッケージを client.go
でインポートすると、このパッケージも自身のディレクトリ内の vendor
フォルダに解決されます。
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here server/ server.go vendor/ github.com/user/package-one/ one.go
同じことが、server.go
ファイルでパッケージをインポートした際にも起こります。
$GOPATH src/ github.com/user/package-one/ one.go myproject main.go vendor/ github.com/user/package-one/ one.go client/ client.go vendor/ github.com/user/package-one/ server/ server.go <-- Importing package-one from here vendor/ github.com/user/package-one/ <-- resolves to here one.go
Understanding Dependency Management
Go
がインポートと販売をどう処理するかについてこれらすべてを学んだので、いよいよ依存関係について説明しましょう。
現在、私が自分のプロジェクトで依存関係を管理するために使用しているツールは godep
と呼ばれています。 これは非常に人気があるようで、私にとってはうまく機能しているので、これを使用することを強くお勧めします。
godep save
を実行すると、godep
は現在の依存関係のリストを Godeps/Godeps.json
に保存し、それらのソースコードを vendor
フォルダにコピーします。
ここで、vendor
フォルダとその内容をコミットすることで、パッケージを実行する際に誰もが同じパッケージの同じバージョンを持つことを確実にすることができます。
もうひとつの興味深いコマンドは godep restore
で、これは Godeps/Godeps.json
ファイルで指定されたバージョンを $GOPATH
にインストールします。
依存関係を更新するために必要なことは、(以前説明したように)go get -u
で更新し、godep save
で Godeps/Godeps.json
ファイルを更新して vendor
ディレクトリにファイルをコピーしています。
Go が依存関係を処理する方法についての考察
このブログ記事の最後に、Go が依存関係を処理する方法についての私自身の意見も追加したいと思います。 また、パッケージを取得する分散化された方法を得たので、エコシステム全体が独立して機能するようになりました。
しかし、分散化されたパッケージ管理にはコストがかかり、この「ネットワーク」の一部であるすべてのノードを制御することはできません。 例えば、誰かが自分のパッケージを github
から持ち出すと決めた場合、 何百、何千、あるいは何百万ものビルドが突然失敗し始める可能性があります。
Go の主な目標を考慮すると、これは完全に理にかなっており、完全に公正なトレードオフです。 もちろん、パッケージの特定のバージョンをフェッチするために git タグを使用し、ユーザーがパッケージが使用するバージョンを指定できるようにするなど、いくつかの改良がなされるかもしれません。 また、ソース管理でチェックアウトする代わりに、これらの依存関係をフェッチできるのはクールでしょう。 これにより、vendor
ディレクトリのコードへの変更のみを含む汚い差分を回避し、リポジトリ全体をよりクリーンで軽量にすることができます。
連絡してください!
何か疑問や考えがある場合、または私が書いたことに同意できない場合、以下のコメントで私と共有するか、Twitter で @thewizardlucas に連絡してください。 あなたの意見を聞き、私が何か間違いを犯した場合は修正したいと思います。
最後になりましたが、GopherCon 2016 での大宮英知の素晴らしい講演をぜひご覧ください!Go ベンダーリングの仕組みについて説明し、その内部構造についての詳細も指摘しています。