Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【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 を管理する手段を提供していたわけではなかった。そのため、glidedepといったサードパーティツールが使用されていた。

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 モードでのモジュール管理先ディレクトリは任意に変更可能らしい、が変え方がわからない。要追記。

参考文献

モジュール・golang / go Wiki・GitHub

Go 言語の依存モジュール管理ツール Modules の使い方 | MMM ブログ

Go 1.13 に向けて知っておきたい Go Modules とそれを取り巻くエコシステム

go1.11 の modules の使い方について

Go と vendoring

Glide: Vendor Package Management for Golang

GitHub - golang/dep: Go dependency management tool

【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.orgGoogle が提供するミラーサーバー。 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 にそのリポジトリを設定するか、 GOSUMDBoff を設定する。

$ 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)
    }
}

%wverb を 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)
}

参考文献

errors - The Go Programming Language

【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

参考: Golang でのエスケープ処理とその解析

値/ポインタ渡しとエスケープ処理の声質を考慮した上で、関数の引数渡しの方針を個人的に決めてみた。なお、以下では基本型の値は特別な事情がない場合は値渡しするものとし、構造体型値をどう渡すかについて考える。

関数引数渡しの個人的方針

値渡しするとき

引数の構造体型が小さい場合

構造体のサイズが小さい場合、値コピーによるコストよりも、関数スタックフレーム領域からヒープ領域への割り当てコストが大きくなる。そのため、コピーコストを恐れて参照渡しするよりも素直にコピーしたほうがパフォーマンスに寄与する場合もある。また、値渡しのコピーによる副作用(ポインタ型プロパティをいじらない場合)回避の恩恵も受けられる。

Map, Slice, Channel などの参照型

型自体が実体となるデータへの参照+メタデータからなる型であり、この型の値そのもののサイズは肥大化しないので、基本値渡しする。

ポインタ渡しするとき

関数内で引数の値を変更する場合

sort 関数のように、仮引数値に対する変更を呼び出し元実引数値に反映したい場合は必然的にポインタ渡しになる。 個人的には副作用がある実装なので、あまり多用はしない。。

引数の構造体型が大きい場合

値渡しのコピーが無視できない場合。構造体のプロパティ値が多いときや、プロパティの型のサイズが大きい場合など。 ただし、ポインタ渡しにしたほうがよい構造体サイズの基準はないので、チーム内で規約として予め決めておくとやりやすいかもしれない。

関数呼び出しの回数が多い

実引数の呼び出し回数が多くなる、あるいはそれが予測できる場合は、コピーが頻発するため引数を参照渡しとしておく。あるいは実装内容によってはそもそも処理をインライン展開したほうが良いかもしれない。


とりあえず、現状は上記の基準で関数引数の型を決めている。 Java の名残で、以前は特別な理由がない限り構造体型はひたすらポインタ型で渡していたが、大きいサイズでなければ値渡しするようになってきた。

参考文献

Pass by pointer vs pass by value in Go

【Golang】エスケープ処理とその解析

Golang の memory allocation

GolangC/C++などと異なり、いい感じに変数のメモリ領域を割り当ててくれる。
例えば、以下のようなコードでも Golang は動作する。

type Hoge struct {}

func NewHoge() *Hoge {
    h := Hoge{}
    return &h
}

NewHoge() 内で生成される構造体値の変数 h の参照がそのまま返戻されている。もし、 hNewHoge 用の関数スタックフレーム内に割り当てられた変数であれば、関数から 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() を呼び出しても何も起こらない。

Closable

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() して問題ない。