【Golang】Go ModulesのGo依存パッケージ管理
Go 1.11 から導入された Go Modules
Go 1.11 より、Go 依存パッケージの解決のための Go Modules の概念が導入された。簡単に言うと、Go 公式でサポートされた npm
のようなパッケージバージョン管理ツール。
Go プロジェクト配下に見受けられる go.mod
go.sum
と、それにまつわる依存パッケージの話でちょっと混乱してきたので、過去の経緯から一旦整理してまとめてみました。
基本に立ち返って、パッケージのインストール
基本的な Go パッケージのインストールは、 go get
で実施する。
$ go get github.com/hoge/fuga
ローカルに置きたいプロジェクトにせよ依存ライブラリが欲しいにせよ、基本的にはこの方法でパッケージを取得していたし、今もそうする。
従来(Go 1.11 以前)の依存パッケージ管理
Go 1.11 以前では、go get
で取得されたパッケージは $GOPATH
配下に置かれていた。コンパイラは、このディレクトリに配置されたライブラリを参照し、パッケージ間の依存関係を解決していた。
しかし、$GOPATH
配下には最新のパッケージしか管理されず、特定のバージョンのパッケージを使用したいときに依存関係を解決できなかった。そこで Go プロジェクト配下に vendor
というディレクトリを作り、そこに所望のバージョンの依存パッケージを置くことで解決した。Go コンパイラはコンパイル時、 $GOPATH
配下より vendor
配下のパッケージを優先的に見てくれる。この vendor
配下でのパッケージバージョン管理は vendoring
と呼ばれていたようだ。
当時、Go 言語自体がこの vendoring を管理する手段を提供していたわけではなかった。そのため、glideやdepといったサードパーティツールが使用されていた。
Go Modules (Go 1.11 以降)におけるパッケージ管理
Go 1.11 になり、依存パッケージのバージョン管理の機能 Go Modules を公式が提供することになった。元々はVersioned Go Command (vgo)と呼ばれるバージョン管理 Go のプロトタイプがあり、Go 公式が 1.11 より Go Modules としてこれをサポートしたもの。
既に依存パッケージのバージョン管理手段があったとはいえ、公式が直々に提供しているのであればそちらを使用したい。 go fmt
のような Go の思想の前例もあり、新規の Go プロジェクトでは Go Modules による依存パッケージバージョン管理が多く見られる。
Go Modules の概念
Go Modules では、以下の2つのパッケージ管理モードがある。
- GOPATH モード
- module aware モード
これらのモードは環境変数 GO111MODULE
によって切り替えられる(詳細は後述)。
GOPATH モード
Go1.11 と同様のパッケージ管理を提供するモード。
go get
により $GOPATH
配下にパッケージが置かれ、管理およびビルド時の参照はこのディレクトリに対して行われる。管理されるパッケージのバージョンは最新版のみなので、 vendor
によるバージョン管理を要する。
module aware モード
Go Modules の概念に対応したモード。
go get
により取得したパッケージを $GOPATH/pkg/mod
配下に置く。module aware モード中は、Go プロジェクトをどこに配置していようが $GOPATH/pkg/mod
配下のパッケージを参照する。
ちなみに、Go Modules では「モジュール」と呼ばれる概念を使用している。標準ライブラリ以外のパッケージをモジュールと呼ぶが、一方でバージョンによって同じパッケージも別のモジュールとして扱う。
module aware モードでの依存解決と、モジュールの作り方
module aware モードでは、パッケージの管理先ディレクトリが変わるだけではなく、Go プロジェクト(モジュール)が必要とするパッケージのダウンロードも自動化してくれる。
その前に、Go プロジェクトをモジュールとして初期化する必要がある。Go プロジェクトをモジュール化するには、 go mod init
を実行する。
$ go mod init
するとプロジェクトルートに go.mod
ファイルが作成される。ファイルにはモジュール名と、使用する Go ランタイムのバージョンが書かれている。
module github.com/rennnosuke/forblog go 1.13
このプロジェクトに、外部のモジュールを参照するコードを追加し、ビルドしてみよう。
package main import ( "fmt" "rsc.io/quote" ) func main() { fmt.Println(quote.Hello()) }
$ go build hello.go
すると、なんと勝手に必要となる依存モジュールをダウンロードし始める。
go: finding rsc.io/quote v1.5.2 go: downloading rsc.io/quote v1.5.2 go: extracting rsc.io/quote v1.5.2 go: downloading rsc.io/sampler v1.3.0 go: extracting rsc.io/sampler v1.3.0 go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c go: finding rsc.io/sampler v1.3.0 go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
module aware モードでは、ビルドしたモジュールの依存関係から必要な外部モジュールを自動的に割り出し、インストールしてくれる。この依存モジュールは先程の go.mod
に記述される。
module github.com/rennnosuke/forblog go 1.13 require rsc.io/quote v1.5.2
npm
で言うところの package.json
に依存パッケージが記述された状態で npm i
しているようなもの。
Go Modules が導入される前と比較し、格段に依存パッケージを取得しやすくなった。また Go Modules 内ではバージョン別にパッケージが管理されるため、 vendor
も不要になる。
この依存パッケージ解決を繰り返していると、不要なパッケージが溜まってくる。
不要なパッケージを削除するには、 go mod tidy
を実行する。
$ go mod tidy
GOPATH モード・module aware モードの切り替え
先述の通り、GOPATH モードと module aware モードを切り替えるには、GO111MODULE
変数の値を変更する。 GO111MODULE
の値には次の 3 種類がある。
- on
- off
- auto
on
module aware モードを有効にする。
off
GOPATH モードを有効にする。
auto
バージョンによって動作を変える。
Go1.12 以前の場合、カレントディレクトリが $GOPATH
配下であれば GOPATH モードになり、そうでなければ module aware モードになる。
ただし Go1.13 からは、auto
の状態でもカレントディレクトリに go.mod
が存在する場合には module aware モードとして振る舞う。
Go モジュールのチェックサム
Go Module 内に生成される go.sum
には依存モジュールのチェックサムが記録される。このチェックサムを使用して、依存モジュールの内容に変更があったかどうかを検出できる 。これにより、インストールするパッケージが改ざんされたものかどうかをチェックできる。
初回インストール時のパッケージのチェックサム検証はできないが、Go1.13 よりチェックサム DBからチェックサムを参照することで初回パッケージのチェックサムも検証できるようになった。
module aware モードでの go get
module aware モード上で go build
して依存関係解決できるからと言って、 go get
の重要性は依然変わらない。例えば Go プロジェクト内で新たに外部パッケージを使用したい場合、npm install [package]
のように新しく依存関係に追加+パッケージインストールしたくなる。
module aware モードで go get
を実行すると、従来どおりパッケージ(モジュール)はインストールされるが、保存先は module aware モード管理上のディレクトリになる。また go.mod
go.sum
に新しい依存パッケージとして記録される。
Go 1.14 以降における Go Modules への公式見解
Go 1.14 では、モジュールサポートは実稼働で使用できる状態にあると見なされ、すべてのユーザーは他の依存関係管理システムからモジュールに移行することが推奨されます。
来る Go1.14 では、すべての Go ユーザが module aware モードへ移行することが推奨されている。移行が高コストな Go パッケージなどでない限り、Go Modules への移行がどんどん進んでいくものと思われる。
所感、メモ
Go プロジェクトに途中から参画したときに、何も考えずに必要なパッケージがすぐインストールできてビルドできるのは本当にありがたい。
module aware モードでのモジュール管理先ディレクトリは任意に変更可能らしい、が変え方がわからない。要追記。
参考文献
Go 言語の依存モジュール管理ツール Modules の使い方 | MMM ブログ
Go 1.13 に向けて知っておきたい Go Modules とそれを取り巻くエコシステム
【Golang】Go 1.13 モジュールミラーリングサーバーとチェックサム DB
Go 1.13 より、Go モジュール関連のサポートが強化された。主な機能は以下の2つ。
依存モジュールのミラーリング
Go 1.13 では、モジュールリポジトリのミラーリングサーバーを指定できるようになった。 GOPROXY
にミラーリングサーバーの URL を指定することで、指定したサーバーにキャッシュが置かれ、go get
などを実行した際にミラーリングサーバーのキャッシュをインストールするようになる。
Go 1.13 での GOPROXY
のデフォルト値は以下のようになっている。
GOPROXY="https://proxy.golang.org,direct"
proxy.golang.org
は Google が提供するミラーサーバー。 direct
を設定した場合、ミラーサーバーを介さず直接リポジトリからインストールする。
もし、一部のモジュールをキャッシュさせたくない場合には、 GOPRIVATE
にモジュールリポジトリ URL を設定する。
$ go env -w GOPROXY=github/hoge/fuga
また、ミラーリングそのものを避けたい場合はミラーリングdirect
を指定すれば無効化できる。
$ go env -w GOPROXY=direct
モジュールのチェックサム検査機構
Go 1.11 より、 go.sum
ファイルに依存パッケージのチェックサム一覧が書かれるようになった。これにより、 go get
などでパッケージをインストールした際、ダウンロードされたパッケージのチェックサムと比較することでパッケージの改ざん検出を行う。
Go プロジェクトを新規に作成したとき、 go.sum
にはチェックサムはなく、改ざん検出も行えない。これを防ぐため、Go モジュールのチェックサムを保存する DB にチェックサムを問い合わせる。
チェックサム保存先 DB は GOSUMDB
に設定する。デフォルト値は GOPROXY
同様公式が提供する DB サーバー URL になる。
GOSUMDB="sum.golang.org"
ただしチェックサム DB の URL を設定していると、 パッケージインストール時に
これはプライベートなリポジトリに Go モジュールを管理している場合などに発生する。そのようなモジュールを扱う場合、 GOPRIVATE
にそのリポジトリを設定するか、 GOSUMDB
に off
を設定する。
$ go env -w GOSUMDB=off
参考文献
Module Mirror and Checksum Database Launched - The Go Blog
Go モジュールのミラーリング・サービス【正式版】 — プログラミング言語 Go | text.Baldanders.info
【Golang】Go1.13 errors ライブラリによるerror値の取り扱い
error
のラッピング
Go 1.13 より、 fmt.Errorf
が error インタフェースを実装する値のラッピングをサポートした。この機能により、ある error 値を別の error に内包する事ができる。
func UpdateHoge(hoge: Hoge) error { err := app.UpdateHoge(hoge) if err != nil { return fmt.Errorf("Internal Server Error : %w", err) } }
%w
verb を format 内に含めることで、error 文字列内に第 2 引数以降にとる error 文字列を含めつつ、内部に error を保持させることができる。新規に作成した error がラップする error を取り出すには Unwrap()
関数を使用する。この関数は同じく Go 1.13 からサポートされるライブラリ errors
内に含まれる。
func Update(){ hoge := NewHoge() err := UpdateHoge(hoge) if err != nil { panic(errors.Unwrap(err)) } }
この errors.Unwrap(error)
関数は、引数に取る error が Unwrap()
を実装していればその関数が返す error 値を、そうでなければ nil
を返す。そのため、自前の error ラッピング構造体を定義したい場合、 Unwrap()
関数を定義する。
type AppError struct { err error msg string code ErrorCode } func (e AppError) Error() string { return e.msg } func (e AppError) Unwrap() error { return e.err }
fmt.Errorf()
のラッピングよって生成される error もこの Unwrap()
を定義している様子。
cerr := errors.New("child error") perr := fmt.Errorf("parent error : %w", cerr) fmt.Println(perr) // parent error : child error fmt.Println(errors.Unwrap(perr)) // child error fmt.Println(errors.Unwrap(cerr)) // <nil>
error の実装する Unwrap()
から取得できる error もまた、 Unwrap()
を実装しうる。この Unwrap()
の連鎖から取得できる一連の error を公式では err's chain と呼んでいる。この chain によって、複数の error 表現をコード内で取り回すことができる。
err's chain
内 error の有無チェック
errors
パッケージでは、err's chain のためのの比較用関数も提供する。
errors.Is()
は、第 1 引数の err's chain に第 2 引数の error が含まれるかどうかを bool で返す。
cerr := errors.New("child error") perr := fmt.Errorf("parent error : %w", cerr) fmt.Println(errors.Is(cerr, cerr)) // true fmt.Println(errors.Is(cerr, perr)) // false fmt.Println(errors.Is(perr, cerr)) // true
err's chain
に含まれる error の参照
errors.As()
関数は、第 1 引数にとる err's chain のうち、第 2 引数と一致する error 型の値があれば、その値で第 2 引数の参照先を置き換えて true
を返す。すなわち、第 2 引数は error ポインタ型となる。第 2 引数に他の型の値、及びポインタが渡されればパニックになり、nil が渡されれはfalse
を返す。
cerr := errors.New("child error") perr := fmt.Errorf("parent error : %w", cerr) fmt.Println(errors.As(perr, &cerr)) // true fmt.Println(errors.As(perr, nil)) // false
エラーチェーン中に特定の具象 error
が含まれれば特定の処理を実行しつつ具象 error を参照する、といった場合に有用そう。
// id : Int prod, err := GetProduct(id) var aerr AppError if errors.As(err, &aerr) { fmt.Errorf("error - %s", aerr) }
参考文献
【Golang】 関数引数 - 値渡しとポインタ渡しの指針
Golangでの値渡しとポインタ渡しの指針
Golang にはポインタの概念がある。なので、C/C++同様に関数引数型を値型・ポインタ型の2つが指定できる。
func print(s string) { fmt.Println(s) } func printPtr(s *string) { fmt.Println(*s) }
値型で引数を渡した場合、関数スタックフレーム領域に引数値がコピーされる。一方、ポインタ型の場合は参照値のみがコピーされるのみで、実体となる値はヒープ上に存在する。
なお、ある関数のローカル変数を別の関数にポインタ渡しすると、その値はヒープ領域へエスケープされる。
code
package main import "fmt" type Product struct { name string price string } func main() { p := Product{} printProductPointer(&p) } func printProductPointer(p *Product) { fmt.Println(*p) }
build with escape analysis
$ go build -gcflags "-m -l" # github.com/rennnosuke/forblog/20200216 ./func_arg.go:15:26: leaking param content: p ./func_arg.go:16:13: printProductPointer ... argument does not escape ./func_arg.go:16:14: *p escapes to heap
値/ポインタ渡しとエスケープ処理の声質を考慮した上で、関数の引数渡しの方針を個人的に決めてみた。なお、以下では基本型の値は特別な事情がない場合は値渡しするものとし、構造体型値をどう渡すかについて考える。
関数引数渡しの個人的方針
値渡しするとき
引数の構造体型が小さい場合
構造体のサイズが小さい場合、値コピーによるコストよりも、関数スタックフレーム領域からヒープ領域への割り当てコストが大きくなる。そのため、コピーコストを恐れて参照渡しするよりも素直にコピーしたほうがパフォーマンスに寄与する場合もある。また、値渡しのコピーによる副作用(ポインタ型プロパティをいじらない場合)回避の恩恵も受けられる。
Map, Slice, Channel などの参照型
型自体が実体となるデータへの参照+メタデータからなる型であり、この型の値そのもののサイズは肥大化しないので、基本値渡しする。
ポインタ渡しするとき
関数内で引数の値を変更する場合
sort
関数のように、仮引数値に対する変更を呼び出し元実引数値に反映したい場合は必然的にポインタ渡しになる。
個人的には副作用がある実装なので、あまり多用はしない。。
引数の構造体型が大きい場合
値渡しのコピーが無視できない場合。構造体のプロパティ値が多いときや、プロパティの型のサイズが大きい場合など。 ただし、ポインタ渡しにしたほうがよい構造体サイズの基準はないので、チーム内で規約として予め決めておくとやりやすいかもしれない。
関数呼び出しの回数が多い
実引数の呼び出し回数が多くなる、あるいはそれが予測できる場合は、コピーが頻発するため引数を参照渡しとしておく。あるいは実装内容によってはそもそも処理をインライン展開したほうが良いかもしれない。
とりあえず、現状は上記の基準で関数引数の型を決めている。 Java の名残で、以前は特別な理由がない限り構造体型はひたすらポインタ型で渡していたが、大きいサイズでなければ値渡しするようになってきた。
参考文献
【Golang】エスケープ処理とその解析
Golang の memory allocation
Golang は C/C++などと異なり、いい感じに変数のメモリ領域を割り当ててくれる。
例えば、以下のようなコードでも Golang は動作する。
type Hoge struct {} func NewHoge() *Hoge { h := Hoge{} return &h }
NewHoge()
内で生成される構造体値の変数 h
の参照がそのまま返戻されている。もし、 h
が NewHoge
用の関数スタックフレーム内に割り当てられた変数であれば、関数から return したあとに参照するのは不可能になる。が、Golang ではこのような変数の参照を追跡し、自動的にヒープ領域へと割り当てるようコンパイル時に最適化してくれる。そのため上記のコードはコンパイルも通るし、正常に実行される。
この処理はエスケープと呼ばれている。エスケープのおかげで、Golang を書くときに値がどの領域に割り当てられていて、どのように渡してよいのか・いけないのかを気にする必要がほとんど無くなる。こういうところでうっかりミスしがちな自分としては、とてもありがたい。。。
Golang の エスケープ処理を追跡する
ところで、コンパイル時のエスケープの動きを見るには、コンパイラ最適化オプションが使える。上記のようなコードが本当にエスケープされているのか、実際に見てみよう。
CompilerOptimizations · golang/go Wiki
Use -gcflags -m to observe the result of escape analysis and inlining decisions for the gc toolchain.
go build -gcflags "-m -l" target.go
-gcflags "-m"
でビルド時のエスケープ解析が有効になる。
また -gcflags "-l"
でコード上のインライン展開最適化を無効にする。
インライン展開を無効にするのは、エスケープされる変数が最適化されて解析がうまく行かなくなるのを防ぐため。
対象とするコード(memesc.go)
package main import "fmt" func main() { hoge := NewHoge() fmt.Printf("%#v\n", hoge) } type Hoge struct { id string } func NewHoge() *Hoge { h := Hoge{id: "000001"} return &h }
分析開始
$ go build -gcflags "-m -l" memesc.go # command-line-arguments ./memesc.go:15:2: moved to heap: h ./memesc.go:7:12: main ... argument does not escape ./memesc.go:7:13: hoge escapes to heap
1 行目
本来 NewHoge()
関数内ローカル変数であった h
が、返戻値となって外部からの参照を持ちうるために、ヒープ領域へと割り当てられている。
2 行目
main
関数内にはエスケープ対象はいないよ、と言われている。
3 行目
NewHoge()
から返ってきた hoge
はヒープ領域に割り当てられているよ、と言われている。被参照側視点。
バイナリを実行する
実行しても、特に問題はない。
$ ./memesc &main.Hoge{id:"000001"}
標準出力ベースだけれど、コンパイル時にどのように最適化されて、どの変数がヒープに移動しているのか見ることができた。
Golang を書き始めて最初に「すごい!」と思ったのがこの仕組みだった。 改めて、Golang の言語仕様をシンプルたらしめるとても良い仕組みの一つだと思う。
とはいえ、本当に何も考えずに変数を定義したり渡したりしていいかというとそうではなく、この仕組みが働いてしまうことでパフォーマンスに少なからず影響を与える箇所がある(関数の引数渡しなど)。なので、コードレビューの際などにはパフォーマンスを考慮する観点としてエスケープのことを頭の隅においておくと良いかもしれない。
参考文献
【Java】ByteArrayOutputStreamのclose()実装
Closable
Java の Closable インタフェースを実装した具象クラスは、 close()
メソッドを定義する必要がある。
Closable
を実装した抽象クラスに Writer
、継承したインタフェースには OutputStream
があり、これらの抽象 I/O を実装したクラスでは確保した出力先リソースを開放するために close()
メソッドを呼ぶ。
例えば、 Writer
を実装した FileWriter
は、以下のように実装された close()
を使用する。
具体的には、 FileWriter
は継承する OutputStreamWriter
のコンポジットオブジェクトである StreamEncoder
メンバの close()
メソッドを呼ぶ。
public void close() throws IOException { synchronized (lock) { if (closed) return; implClose(); closed = true; } }
排他制御の下、リソースの開放処理が行われる。
ちなみに、 close()
の仕様にもあるように、すでに開放された状態で再度 close()
を呼び出しても何も起こらない。
void close() throws IOException このストリームを閉じて、それに関連するすべてのシステム・リソースを解放します。ストリームがすでに閉じられている場合は、このメソッドを呼び出しても何の効果もありません。 AutoCloseable.close()で説明されているように、クローズが失敗する可能性がある場合は慎重な注意が必要です。IOException をスローする前に、基礎となるリソースを解放することと、Closeable をクローズ済として内部的にマークすることをすることを強くお薦めします。
ByteArrayOutputStream
バイト列を出力する ByteArrayOutputStream
も同様に OutputStream
を実装しており、 close()
を以下のように実装している。
/** * Closing a {@code ByteArrayOutputStream} has no effect. The methods in * this class can be called after the stream has been closed without * generating an {@code IOException}. */ public void close() throws IOException { }
何もしていない。そのため、 close()
をコールしても何も起こらない。
このことから、生成した ByteArrayOutputStream
に対する close
処理は不要。ただし、 OutputStream
のような抽象型でオブジェクトを取り扱う場合でも close()
して問題ない。