Goのエラー処理② panicとrecover
Goのpanicとrecover
Goのエラー処理は原則として例外を使わず、エラーを丁寧に処理すべき方針はわかったとして、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とエラーの関連がわかるように書く
- サブルーチンから返却されたエラーに無い情報は、呼び出し元で追記して最終的なエラーとして返却する(エラーの伝搬)
PythonやJavaなどの例外を常用する言語でプログラムを書いていると、メインルーチンで基底の例外を捕捉し、サブルーチンでより詳しい例外派生クラスを送出する処理を書く手法に慣れているので、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
実際に試した
まず、整数のスライスを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種類に分けることにした。
- targetsの個数が少ない (50000, 50)
- items, targetsともに多い (50000, 50000)
- 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割くらい速い。
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の方が手軽に性能試験を試せて良いものだと思った。
参考リンク
テスト結果に出力される数の読み方については特に以下のふたつの記事が参考になった。
Golangでベンチマークを取ってみた - Qiita
Big Sky :: golang でスライスの先頭に追加する append がなぜ遅いのか
その他
Go言語のベンチマークでパフォーマンス測定 | eureka tech blog
Go でベンチマーク - Block Rockin’ Codes
testing - The Go Programming Language
tools/parse.go at master · golang/tools · GitHub
*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
例えばPythonのDjangoだと、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" の意味を理解すること。
やっていることはモックと本物のデータソースを置き換えることらしい。