今川館

都内勤務の地味OLです

Goのエラー処理② panicとrecover

Goのpanicとrecover

Goのエラー処理は原則として例外を使わず、エラーを丁寧に処理すべき方針はわかったとして、Goの例外機構というものがどういう仕組なのか調べた。

まず、基本に忠実に、golang.jpの説明を読むと、

実践Go言語 - golang.jp

Recover

panicが呼び出されたとき(これには、配列のインデックスが範囲外であるときや、型アサーションに失敗したような暗黙的なものも含む)は、すぐさま、カレントの関数を停止し、ゴルーチンのスタックの巻き戻しを開始します。その途中、遅延指定されている関数をすべて実行します。この巻き戻しがゴルーチンのスタックの先頭にたどり着くと、プログラムは終了します。ただし、組み込み関数recoverを使うことで、ゴルーチンの制御を取り戻し、通常の実行を再開させることが可能です。

panicが発生すると即座に処理を打ち切るので、これが他の言語の例外に相当するものらしい。

「その途中、遅延指定されている関数をすべて実行します」とも言っているので、deferに無名関数を指定してrecoverしないとダメなんじゃないかと思ったら、ご丁寧にその後に明記されていた。

recoverを呼び出すと、巻き戻しを停止し、panicに渡された引数が返ります。巻き戻り中に実行できるコードは、defferで遅延指定された関数内のみなので、recoverは遅延指定された関数内でのみ役立ちます。

Pythonとの比較

例えば、リトライ可能な例外がサブルーチンから送出された場合は例外を握りつぶし、それ以外は上位ルーチンに送出するプログラムをPythonで書くとこんな感じだろう。

class Retryable(Exception):
    pass

try:
    do_something() // 何か例外を送出する可能性がある処理
except Retryable as ex:
    print(ex)
except Exception as ex:
    raise ex

普通にtry-exceptブロック使うでしょう。

対して、Goで似たようなことをやろうとすると、こんな感じだろう。

type Retryable struct{}

defer func() {
	switch p := recover(); p {
	case nil:
		// ここはパニックが起きなかった場合が該当する
		fmt.Println("No error.")
	case Retryable{}:
		err = fmt.Errorf("failed, retry")
		// err = errors.New("failed, retry")
	default:
		panic(p)
	}
}()

Pythonがブロックを扱う方法を採るのに対し、Goはブロックを監視する仕組みではなくあくまでも関数ベースの解決方法を採ることがよく分かる。

「実践Go言語」のページにはこのようにも書いてある。

他言語のブロックレベルのリソース管理に慣れたプログラマには、deferは特異に見えるかも知れませんが、deferのもっとも面白く、強力な活用法は、deferがブロックベースではなく、関数ベースであることによって生み出されます。panicとrecoverのセクションに、この可能性を示す例があります。

やっぱり、recoverはdeferと一緒に使わないと意味がないみたいだ。

また、Pythonのコンテキストマネージャはまさにブロックを監視する仕組みなので、Goで同じことをしようと思ったらdeferを使うのだろう。
とはいえ、Pythonでコンテキストマネージャを作るときはcontextlib.contextmanagerで関数をラップすれば簡単に作れるので、Goの流儀でPythonのコンテキストマネージャを考えると、Pythonには関数ベースの基本的なアプローチの上に、ブロックベースの仕組みに置き換える方法が用意されていると捉えるべきか(謎)。

contextlibを使う場合はこんな感じ。

from contextlib import contextmanager

class Retryable(Exception):
    pass

@contextmanager
def retrying():
    try:
        yield
    except Retryable as ex:
        print(ex)
    # except Exception as ex:
    #     raise ex

with retrying():
    do_something() // 何か例外を送出する可能性がある処理

実際に試した

せっかくなので、エラーを返却する原則的なやり方と、panic/recoverを利用する方法をそれぞれ使い分けるサンプルコードを書いてみた(以下)。

package main

import "fmt"

type Challenger struct {
	count     int
	threshold int
}

type Retryable struct{}

// 非公開メソッドの方はわざとpanicを利用する。
func (c *Challenger) attack() bool {
	if c.count < c.threshold {
		c.count++
		panic(Retryable{}) // パニック、しかしリトライ可能
	}
	if c.count > c.threshold {
		panic("No more chance.")
	}
	c.count++
	return true
}

// 公開メソッドはpanicを捕捉してリカバーをかける。
func (c *Challenger) Attack() (ok bool, err error) {
	defer func() {
		switch p := recover(); p {
		case nil:
			// ここはパニックが起きなかった場合が該当する
			fmt.Println("No error.")
		case Retryable{}:
			err = fmt.Errorf("Failed(%v times)", c.count)
		default:
			panic(p)
		}
	}()
	ok = c.attack()
	return
}

func main() {
	var c *Challenger
	c = &Challenger{0, 3}
	fmt.Println(c.Attack()) // false Failed(1 times)
	fmt.Println(c.Attack()) // false Failed(2 times)
	fmt.Println(c.Attack()) // false Failed(3 times)
	fmt.Println(c.Attack()) // true <nil>
	fmt.Println(c.Attack()) // 以下panic

	// panic: No more chance. [recovered]
	// 	panic: No more chance.
	//
	// goroutine 1 [running]:
	// panic(0x4b9f40, 0xc82000a400)
	// 	/usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6
	// main.(*Challenger).Attack.func1(0xc820039e30, 0xc820039e58)
	// 	/home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:35 +0x2fc
	// panic(0x4b9f40, 0xc82000a400)
	// 	/usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9
	// main.(*Challenger).attack(0xc820039e58, 0x52c678)
	// 	/home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:19 +0xd0
	// main.(*Challenger).Attack(0xc820039e58, 0x0, 0x0, 0x0)
	// 	/home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:38 +0x78
	// main.main()
	// 	/home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:49 +0x52c
	// exit status 2
}

Goのエラー処理① エラー処理の基本的な考え方

Goのエラー処理方針

Goの例外やエラーの処理について勉強中。まず簡単なことからメモ。

例外は原則として使わない・予期される異常処理はすべて手当する

プログラミング言語Go』を読むと、Goに例外の仕組みはあるけど、基本的にエラーを丁寧に処理しないという方針らしい。

(前略)Goはある種の例外機構を持っていますが、バグであることを示す本当に予期されていないエラーを報告するためだけに使われ、頑強なプログラムを構築するためのルーチンで予期されるエラーには使われません。

なので、エラーを処理する関数は戻り値にerror型の値を返すよう実装するのが良いようだ。

エラーの作り方

エラーは組み込みのerror型を利用する。作り方は

  • errors.New("エラーメッセージ")で作る
  • fmt.Errorf("...")で作る

上記二通りあるようだ。(以下、サンプル)

package main

import (
	"errors"
	"fmt"
)

func main() {
	var err error

	err = errors.New("処理に失敗しました") // エラーの原因などが何も無いので、あまり良くないメッセージ
	fmt.Println(err)

	err = errors.New(fmt.Sprintf("処理中に接続が切れたためリトライを開始します(%v回め)", 3))
	fmt.Println(err)

	// 前のエラー内容を引き継いで新規エラーメッセージを追記する場合
	err = fmt.Errorf("%v: リトライ回数が一定に達したため、処理を打ち切ります(%v回試しました)", err, 7)
	fmt.Println(err)
}

エラーメッセージはデバッグ等に役立てるためにも、(引数やローカル変数の内容も併記することが望ましいので)、とりあえずfmt.Errorfを使っていれば良いんじゃないかと思った。

エラーメッセージのルール

これも『プログラミング言語Go』で推奨されているエラーメッセージのルールだが、

  • メッセージに大文字を使わない方が良い(頻繁に連鎖[追記]するから)
  • 改行文字も使わない(grepしにくくなるから)
  • 関数f(x)のエラーは、操作fに対する引数xとエラーの関連がわかるように書く
  • サブルーチンから返却されたエラーに無い情報は、呼び出し元で追記して最終的なエラーとして返却する(エラーの伝搬)

PythonJavaなどの例外を常用する言語でプログラムを書いていると、メインルーチンで基底の例外を捕捉し、サブルーチンでより詳しい例外派生クラスを送出する処理を書く手法に慣れているので、Goのルールになじむまでなかなか骨が折れそうだ。

ただ、Goのポリシーは異常が起きた箇所で確実に手当していく心がけにもつながるんじゃないかと思う。

go buildを試しつつGoのパッケージの規則を覚えた

go buildでバイナリを作る

go buildコマンドを実行すると、カレントディレクトリからmainパッケージを探してバイナリを作ってくれる。

Githubでの管理を念頭に推奨されるプロジェクト構成

  • go getでgithub.comから依存ライブラリを取ってくる
  • importで自己ないし外部プロジェクトのモジュールを参照する

上記2点を考慮すると、以下の記事で紹介されている構成にしておくのが無難らしい。

Golangで自分自身で定義したパッケージをインポートする方法あれこれ - Qiita

$GOPATH/src/github.com/[username]/[appname]
この中で最初から作業をしてしまう.

実際に試してみた

$GOPATH/src/github.com/oyakata/tokyo
├── hello
│   ├── api.go  # helloパッケージ
│   └── main.go # helloパッケージ
└── main.go      # mainパッケージ

まず、tokyo/main.goの内容は以下。

package main

import (
	ikzo "github.com/oyakata/ikzo/hello"
	"github.com/oyakata/tokyo/good"
	tyo "github.com/oyakata/tokyo/hello"
)

func main() {
	ikzo.World() // ikzoのhello.World()を実行
	tyo.World() // tokyoのhello.World()を実行
	tyo.GotoTokyo() // tokyoのhello/api.goに定義した関数
	best.DoBest(1000) // tokyoのgood/main.goに定義した関数
}

わたしは /home/echizen/_go をGOPATHに指定したので、以下の手順でプロジェクトを配備した。

$ mkdir -p /home/echizen/_go/src
$ cd /home/echizen/_go/src
$ git clone git@github.com:oyakata/ikzo.git
$ git clone git@github.com:oyakata/tokyo.git
$ cd tokyo

$ export GOPATH=/home/echizen/_go
$ go build -o tyo  # => tyoという名前のバイナリができる

# あるいは
$ GOPATH=/home/echizen/_go go build -o tyo

$ ./tyo  # tokyo/main.goのmainに書いた処理が実行される

イレギュラーなことも試してみた

ルール通りにやれば何ということもなくビルド&実行できたのだが、いくつかルールを破ってみた場合にどうなるのか試してみた。

対象ディレクトリ配下に複数のmainパッケージのモジュールを置く

例えば、tokyo/の直下にmain_tyo.goとmain_ikzo.goという、mainパッケージのモジュールを2つ置いてgo buildしてみる。

これはコンパイルエラーが出てビルドが失敗する。

echizen@Seigaku:~/_go/src/github.com/oyakata/tokyo$ go build
# github.com/oyakata/tokyo
./main_tyo.go:5: main redeclared in this block
	previous declaration at ./main_ikzo.go:7

異なる名前のパッケージ宣言を持つモジュールを同じディレクトリの中に置く

例えば、tokyo/helloの中で、main.goはhelloパッケージ、api.goはapiパッケージを宣言してみる。

これもコンパイルエラーになる。

echizen@Seigaku:~/_go/src/github.com/oyakata/tokyo$ go build
main.go:6:2: found packages api (api.go) and hello (main.go) in /home/echizen/_go/src/github.com/oyakata/tokyo/hello

ディレクトリと異なる名前でパッケージ宣言してはいけないのか?

例えば、tokyo/goodディレクトリの中で、bestパッケージを宣言してみる。

これはコンパイルが通る。が、インポートすると best という名前で認識される。

package main

import "github.com/oyakata/tokyo/good"

func main() {
	// good.DoBest(1000)  // パッケージ宣言は best なので、goodという名前では参照できない
	// # command-line-arguments
	// ./main_tyo.go:6: imported and not used: "github.com/oyakata/tokyo/good" as best
	// ./main_tyo.go:13: undefined: good in good.DoBest
	best.DoBest(1000) // これは大丈夫
}

或いは

package main

import good "github.com/oyakata/tokyo/good"

func main() {
	good.DoBest(1000)
}

このように、(ディレクトリの名前はgoodにもかかわらず)名前付きインポートでわざわざgoodという名前を指定するといった煩雑なことをしなければならない。
それだったら最初からディレクトリの名前とパッケージ宣言は合わせておけよ、というわけで、やっぱり面倒なことをしないに限る。

Goは関数の戻り値に名前を付けられる / deferの驚き

Goは関数を宣言するとき、戻り値に名前をつけることができる。*1

例えば、任意個数の整数を引数にとって合計値を返却するSumという関数を作るとして

func Sum(xs ...int) int {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return acc
}

今までわたしはこう書いていた。

戻り値に名前をつける

ところが、同じSumを、戻り値に名前をつけて定義してみると、実は色々不思議なルールがあることを知った。

戻り値に名前をつけられることを知って、わたしは最初はこのように書いていた。

func Sum(xs ...int) (result int) {
	acc := 0
	for i := 0; i < len(xs); i++ {
		acc += xs[i]
	}
	return acc
}

要するに、関数のシグニチャに宣言したresultという名前は単なるお約束で、触っちゃいけないと思っていたのだ。

名前をつけた戻り値は変数として利用して構わない

だが、どうやら名前をつけて定義した戻り値はゼロ値で初期化された変数として与えられ、関数本体から使って良いらしい。

つまり、こういうことをしても全く構わない。

func Sum(xs ...int) (result int) {
	for i := 0; i < len(xs); i++ {
		result += xs[i]
	}
	return result
}

戻り値に名前をつけて定義した関数はreturnだけで終了しても構わない

更に驚いたのが、戻り値に名前をつけておくと、returnだけで処理を終えても構わないのである。

以下のコードも当然コンパイルが通って動く。

func Sum(xs ...int) (result int) {
	for i := 0; i < len(xs); i++ {
		result += xs[i]
	}
	return
}

多値を返す場合も同様で、こんなことをしても(意味のない処理ではあるが)コンパイルは通り動いてくれる。

func Fn() (x int, y string, z [2]int) {
	return
}

Fn() // => 0 "" [0, 0]

deferと組み合わせる

deferキーワードを付けた文は、関数の処理修了後まで保留される。よく、コネクションやファイルのClose()に使えと言われる、アレである。

例えば上記のSum関数は、こう書いても一応正しく動く。

func Sum(xs ...int) (result int) {
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

deferを使って戻り値をプリントしてみる

これをdeferで戻り値をプリントするよう変えて動かすと、

func Sum(xs ...int) (result int) {
	defer fmt.Printf("Sum> result = %s\n", result)
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

Sum(1, 2, 3, 4) // => 10

[出力内容]

Sum> result = %!s(int=0)

この通り、よく考えれば当然なのだが、deferを行った時点のresultを束縛して後回しにされるので、初期値0が表示されてしまう。

無名関数を使う

無名関数で更に遅延すると期待通りの内容が表示される。

func Sum(xs ...int) (result int) {
	defer fmt.Printf("Sum> result = %s\n", result)
	defer func(){ fmt.Printf("Sum> result = %s\n", result) }()
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}

Sum(1, 2, 3, 4) // => 10

[出力内容]

Sum> result = %!s(int=10)
Sum> result = %!s(int=0)

ここで無名関数から出力した結果は6ではなく10になっている点に注意。
つまり、returnの後にresultの評価にワンチャンあるのだ。

また、deferは複数スタックされた場合は後ろから順に実行されていくので、無名関数を使って10が出力される処理の方が先に表示される。

deferの値を更に書き換えることも可能

上記コードでは無名関数の中から変数resultを参照できるので、内容を書き換えることもできる。
よって、以下のことも可能である。

func Sum(xs ...int) (result int) {
	defer func(){
		result -= 500
		fmt.Printf("Sum> result = %s\n", result)
	}()
	for _, x := range xs[:len(xs)-1] {
		result += x
	}
	return result + xs[len(xs)-1]
}
Sum(1, 2, 3, 4) // => -490

[出力内容]

Sum> result = %!s(int=-490)

deferはよく知らずに使うとろくでもないことをやらかしそうな気がした。

*1:これを「名前付き結果」とか「名前付き結果パラメータ」と呼ぶらしい。

Goのベンチマークテストを試した

BenchmarkTestの読み方

Goではモジュールの性能試験を行う仕組みが最初から用意されていて、testing.Bを使えば良いらしい。

[Goのベンチマークテストのルール]

  • テストの名前はBenchmarkチョメチョメとする ("Benchmark"で名前を始める)
  • 引数は *testing.B 、つまりBのポインタ型を受け付ける
  • 実行するときは go test に "-bench" オプションをつける

決まりを守ってプログラムを書くと以下のような出力が得られる。

PASS
BenchmarkTest1	    5000	   1573757 ns/op
BenchmarkTest2	     500	   2457098 ns/op
ok  	_/home/echizen/tokyo/app/bench	11.021s

出力結果の読み方

まず、出力結果の読み方がわからなかったので調べたら大変親切にまとめている記事が既にあって助かった。

Golangでベンチマークを取ってみた - Qiita

// 関数の実行回数、有用な結果が得られるまで実行される
// 多ければ多いほど良い
2000000

// 1回の実行にかかった時間
// 少ないほど良い
815 ns/op

つまり、上記の出力結果だと、BenchmarkTest1は5000回試行して有意な結果が出て、1回あたり1,573,757ナノ秒かかるという意味。
対して、BenchmarkTest2は500回で有意な結果が出て、1回あたり2,457,098ナノ秒かかる。

ナノ秒って何秒?

まず、わたしには「ナノ秒」という単位が見慣れない。
ナノは10の-9乗らしいので、これをかけて秒数に直すと、

  • BenchmarkTest1は (10 ** -9) * 1573757 = 0.001573757秒
  • BenchmarkTest2は (10 ** -9) * 2457098 = 0.002457098秒

ということでBenchmark2の方が1より5割ほど遅いという意味らしい。

実際に試した

まず、整数のスライスを2つ受け取って、第2引数のスライスに出現する整数が第1引数の中にいくつ含まれるか数える関数を作る。
実装はふたつ用意することとし、一方をスライスを前から順に調べていく方法で、他方をmapを使う方法で作ったものが以下である。

[main.go]

package bench

type Pair [2]int

// itemsの中からtargetに該当するものを数える
func CountUp1(items []int, targets ...int) []Pair {
	result := make([]Pair, 0, len(targets))

	var acc int
	for _, x := range targets {
		acc = 0
		for _, y := range items {
			if x == y {
				acc++
			}
		}
		result = append(result, Pair{x, acc})
	}
	return result
}

func CountUp2(items []int, targets ...int) []Pair {
	xs := make(map[int]int, len(items))
	for _, x := range items {
		xs[x] += 1
	}

	result := make([]Pair, 0, len(targets))
	for _, y := range targets {
		result = append(result, Pair{y, xs[y]})
	}
	return result
}

次に、テストの条件(itemsの個数, targetsの個数)を3種類に分けることにした。

  1. targetsの個数が少ない (50000, 50)
  2. items, targetsともに多い (50000, 50000)
  3. itemsの個数が少ない (50, 50000)

テストコードは以下。

[main_test.go]

package bench

import (
	"math/rand"
	"testing"
)

func makeParams(P int, Q int) (x, y []int) {
	var items []int
	var targets []int
	items = make([]int, P)
	for i := 0; i < len(items); i++ {
		items[i] = rand.Intn(Q)
	}

	targets = make([]int, Q)
	for j := 0; j < len(targets); j++ {
		targets[j] = j
	}
	return items, targets
}

const ITEMS1 = 50000
const TARGETS1 = 50

const ITEMS2 = 50000
const TARGETS2 = 50000

const ITEMS3 = 50
const TARGETS3 = 50000

func BenchmarkTest1Using1(b *testing.B) {
	items, targets := makeParams(ITEMS1, TARGETS1)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp1(items, targets...)
	}
}

func BenchmarkTest1Using2(b *testing.B) {
	items, targets := makeParams(ITEMS1, TARGETS1)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp2(items, targets...)
	}
}

func BenchmarkTest2Using1(b *testing.B) {
	items, targets := makeParams(ITEMS2, TARGETS2)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp1(items, targets...)
	}
}

func BenchmarkTest2Using2(b *testing.B) {
	items, targets := makeParams(ITEMS2, TARGETS2)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp2(items, targets...)
	}
}

func BenchmarkTest3Using1(b *testing.B) {
	items, targets := makeParams(ITEMS3, TARGETS3)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp1(items, targets...)
	}
}

func BenchmarkTest3Using2(b *testing.B) {
	items, targets := makeParams(ITEMS3, TARGETS3)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CountUp2(items, targets...)
	}
}

実行結果は

(_go)echizen@Seigaku:~/tokyo/app/bench$ go test -bench .
testing: warning: no tests to run
PASS
BenchmarkTest1Using1	     500	   3207881 ns/op
BenchmarkTest1Using2	     500	   2507589 ns/op
BenchmarkTest2Using1	       1	2415230671 ns/op
BenchmarkTest2Using2	     100	  11777956 ns/op
BenchmarkTest3Using1	     500	   3034820 ns/op
BenchmarkTest3Using2	    1000	   1892243 ns/op
ok  	_/home/echizen/tokyo/app/bench	15.183s

上記から、このような判断ができる。

  • 類型1(50000, 50)だと実装1, 2は互いに有意な結果が得られるまで500回かかったが、実装2の方が3割くらい速い。
  • 類型2(50000, 50000)だと、実装1はたった1回やれば十分と判断され、実装2に比べて240倍くらい遅い。
  • 類型3(50, 50000)だと、実装2は実装1の2倍の1000回で測定打ち切りになり、実装1より4割くらい速い。

testing.Bから取れるNとは?

上記サンプルコードでtesting.B型の引数bからNという値を取ってループを回しているが、最初これも何に使うのかわかっていなかった。

これについては『プログラミング言語Go』に書いてあった説明が非常にわかりやすいので引用しておく。

ベンチマークの実行系は操作に要する時間を最初は分かっていませんので、Nの小さな値を使って最初の測定を行い、それから安定した時間の計測ができるだけの十分な大きさの値を推定します。

何と裏側で打診的にテスト対象を実行した後、本格的に性能を計測しているそうだ。

Pythonでやる場合は?

組み込みモジュールにtimeitというやつがあるので、これを使うのがお手軽なんだと思う*1

[bench.py]

# -*- coding:utf-8 -*-
import random


def countup1(items, targets):
    L = []
    for target in targets:
        count = 0
        for x in items:
            if x == target:
                count += 1
        L.append((target, count))
    return L


def countup2(items, targets):
    L = []
    tmp = {x: 0 for x in targets}

    for x in items:
        tmp[x] += 1
    return [(x, tmp[x]) for x in targets]


def make_params(x, y):
    return (
        [random.randint(1, y) for i in range(x)],
        list(range(1, y + 1)),
    )


P1A, P1B = make_params(50000, 50)
P2A, P2B = make_params(50000, 50000)
P3A, P3B = make_params(50, 50000)


if __name__ == '__main__':
    import timeit

    items = (
        ('P1A', 'P1B', (500, 500)),
        ('P2A', 'P2B', (1, 100)),
        ('P3A', 'P3B', (500, 1000)),
    )

    G = globals()

    for x, y, ns in items:
        one, two = ns
        result1 = timeit.timeit(
            'countup1({}, {})'.format(x, y),
            setup='from __main__ import countup1, P1A, P1B, P2A, P2B, P3A, P3B',
            number=one,
        )
        print('1({}, {})>\t{}\t{}s'.format(len(G[x]), len(G[y]), one, result1))

        result2 = timeit.timeit(
            'countup2({}, {})'.format(x, y),
            setup='from __main__ import countup2, P1A, P1B, P2A, P2B, P3A, P3B',
            number=two,
        )
        print('2({}, {})>\t{}\t{}s'.format(len(G[x]), len(G[y]), two, result2))

[結果]

(py3k)echizen@Seigaku:~/py3k$python bench.py
1(50000, 50)>	500	46.40978667300078s
2(50000, 50)>	500	2.749026679999588s
1(50000, 50000)>	1	94.42981744899953s
2(50000, 50000)>	100	2.782400592999693s
1(50, 50000)>	500	51.12394202299947s
2(50, 50000)>	1000	13.445461807001266s

Pythonのtimeitは試行回数をnumberという名前の引数に指定して動かす。
何回実行してどの程度の速度の差が出るのか等、自分で判断してテストする必要がある。

こうしてみるとGoのtesting.Bの方が手軽に性能試験を試せて良いものだと思った。

*1:試したらすごく遅かったんだけど何かミスってますかね・・

GoのBuild Constraints

go buildでモックモードの成果物をビルドできる(?)

ことの始まりは以下のスライドでデータ取得処理をモックと本物とで入れ替える話を読んだことなのだが、

https://speakerdeck.com/azihsoyn/implementing-go-go-go

スライドにはご丁寧にサンプルコードがリンクしてあって、

https://github.com/azihsoyn/IDDD_go_sample

mock mode

Requires

nothing

$ go build +build=mock
$ ./IDDD_go_sample


説明書きを読むと、 "+build=mock" ってオプションを付けると、モックのデータで稼働させられますよ、ということらしい。

この、"+build=mock" という、いかにも思わせぶりな記述をすると、どういう仕組みでデータ取得処理がモック/本物と切り替わるのかわからなかったので調べた。

謎の記述は「Build Constraints」という仕組み

まことに情けないことに、結局 "+build=チョメチョメ" と指定してもわたしの手元では期待通り動かなかったのだが、どうも "-tags チョメチョメ" と指定すると同じ動きになることは理解した。

Goはビルドのときにビルド対象のソースコードをある程度コントロールできて、そういう仕組みを「Build Constraints」と呼ぶらしい。

build - The Go Programming Language

で、さきほどのサンプルコードをよく見ていくと、

// +build mock

// +build !mock

このような記述がデータ取得処理を担うプログラムの先頭に書いてあった。

https://github.com/azihsoyn/IDDD_go_sample/blob/4f6995a6b9e16e09f20262e19418d8f0e2075eeb/internal/repository/article/article.go#L1
https://github.com/azihsoyn/IDDD_go_sample/blob/4f6995a6b9e16e09f20262e19418d8f0e2075eeb/internal/repository/article/repository.go#L1

例えばPythonDjangoだと、runserverやtestコマンドに "--settings=settings.production" とか "--settings=settings.dev" などとオプションを指定して設定を切り替えて稼働させるが、Goはコンパイル言語なのでビルド時に同様の解決が可能であることを知った。

試してみた

試しに以下の構成でプログラムを書いて試してみると、確かに実行コードの内容を切り替えることができた。

app
├── main.go
└── cfg
     ├── cfg_dev.go
     └── cfg_prod.go

[main.go]

package main

import (
	"./cfg"
	"fmt"
)

func main() {
	fmt.Println(cfg.HELLO)
}


[cfg/cfg_dev.go]

// +build debug

package cfg

const HELLO = "Hello, dev-world."


[cfg/cfg_prd.go]

// +build !debug

package cfg

const HELLO = `*** Danger!! ***
Hello, world.
`

[ビルド/実行結果]

(_go)echizen@Seigaku:~/tokyo/app$ go build
(_go)echizen@Seigaku:~/tokyo/app$ ./app 
*** Danger!! ***
Hello, world.

(_go)echizen@Seigaku:~/tokyo/app$ go build -tags debug
(_go)echizen@Seigaku:~/tokyo/app$ ./app 
Hello, dev-world.

Repositoryパターン

あと、IDDD_go_sampleのサンプルコードでなんでデータの取得処理に「repository」という名前をつけるのか不思議に思ったのだが、これは「Repositoryパターン」というパターンがから来ているらしい。

ASP.NET MVCの開発応用編1 ~リポジトリパターンをマスターする~ (1/5):CodeZine(コードジン)

リポジトリパターンはアプリケーション(Controller)からデータアクセスロジックの部分だけを抜き取ったリポジトリインターフェイスを実装したリポジトリクラスに接続します。こうすることで、アプリケーション側はEDMやDataSetなどのデータプロバイダを意識することなくデータのやり取りを実現できます

参考リンク

"// +build チョメチョメ"の次に空行を入れないとダメなルールらしいので注意。

GoのBuild Constraintsに関するメモ - Qiita
go build -tagsを使ってRelease/Debugを切り替える - flyhigh
build - The Go Programming Language

Goの抽象構文木の利用例を読んだ

goにはコードの自動生成ツールが付属しているらしい。

Go と reflect と generate
http://qiita.com/naoina/items/7966f73f3a807b3d25d6

ここで「AST からコード生成する方法」という節の内容に興味を持った。

文字列テンプレートからコードを生成するのではなく、抽象構文木から生成する方法である。

IdentとかFuncDecとかいう概念がよくわからないので抽象構文木の使い方でぐぐったらt2yの書いた記事がヒットしてしまった。

Python の ast モジュール入門 (抽象構文木を辿る)
http://qiita.com/t2y/items/0964d01bf3db0233e3c1

めぼしい記載は無かったが、これを読んでいて、astに特定の決まった構成要素が定められているわけではないことは理解できた。
つまりGoのastのドキュメントを読まなければ自分の知りたいことはわからない、と。
そして、ひとまずGoのastのドキュメントに戻った。

type Ident
     An Ident node represents an identifier.
type Ident struct {
        NamePos token.Pos // identifier position
        Name    string    // identifier name
        Obj    *Object  // denoted object; or nil
}
type FuncDecl
     A FuncDecl node represents a function declaration.
type FuncDecl struct {
        Doc  *CommentGroup // associated documentation; or nil
        Recv *FieldList    // receiver (methods); or nil (functions)
        Name *Ident        // function/method name
        Type *FuncType    // function signature: parameters, results, and position of "func" keyword
        Body *BlockStmt    // function body; or nil (forward declaration)
}
type BasicLit
     A BasicLit node represents a literal of basic type.
type BasicLit struct {
        ValuePos token.Pos  // literal position
        Kind    token.Token // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
        Value    string      // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
}
type CompositeLit
     A CompositeLit node represents a composite literal.
type CompositeLit struct {
        Type  Expr      // literal type; or nil
        Lbrace token.Pos // position of "{"
        Elts  []Expr    // list of composite elements; or nil
        Rbrace token.Pos // position of "}"
}

問題は以下のコードを理解することだ。

        case *ast.Ident:
            if strings.Contains(aType.Name, "PlaceHolder") {
                aType.Name = strings.Replace(aType.Name, "PlaceHolder", structName, 1)
            }
        case *ast.FuncDecl:
            if aType.Recv == nil {
                break
            }
            switch aType.Name.Name {
            case "columns":
                aType.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.BasicLit).Value = strconv.Quote(strings.Join(columns, ", "))
            case "fields":
                recvName := aType.Recv.List[0].Names[0].Name
                clit := aType.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.CompositeLit)
                clit.Elts = clit.Elts[:0]
                for _, c := range columns {
                    clit.Elts = append(clit.Elts, &ast.UnaryExpr{
                        Op: token.AND,
                        X: &ast.SelectorExpr{
                            X:  ast.NewIdent(recvName),
                            Sel: ast.NewIdent(c),
                        },
                    })
                }
            }
        }

Identがメソッドのレシーバーや関数の引数に相当するらしい。
その識別子の名前が "PlaceHolder" を含めば、所定の名前に書き換える、という処理が最初のif節。

次が、FuncDeclは関数の定義を示すらしい。

その関数がレシーバーを持っていなければbreak。メソッドしか書き換えないから普通の関数が来たら処理を打ち切る。

そして、引数の名前が "columns" または "fields" ならばチョメチョメする。

"columns" が来たとき
最初の行をreturnステートメントとみなし、その文に含まれる先頭のリテラル値を書き換える。

"fields"が来たとき
returnステートメントからコンポジット型のリテラルを取り出して内容を書き換える。

だいたいこんな感じのことをやっているんだろう。

どうもGoの名前の略し方が慣れないのだが(そもそもPythonはあまり名前を省略しないので)、

  • Identはidentifierの略
  • FuncDeclはFunction declarationの略
  • BasicLitはBasic type literal(基本データ型のリテラル, intとかstring)の略
  • CompositeLitはComposite type literal(コンポジット型のリテラル, mapとかスライス)の略

ということらしい。

次の目標

次の目標は

https://github.com/azihsoyn/IDDD_go_sample

ここに書いてある

$ go build +build=mock

この "+build=mock" の意味を理解すること。
やっていることはモックと本物のデータソースを置き換えることらしい。