Goのパッケージ、ディレクトリ構造とエラー処理

ここ数日悩んでいるGoのパッケージ、ディレクトリ構造とエラー処理の話。

何気にGoDocにgo buildのドキュメントもあるんだなぁと気付いた。考えてみれば、Goのソースは全て公開されているのだった。goコマンドのソースを眺めてみるとちょっと面白かった。goコマンドのmainは以下になる。

https://golang.org/src/cmd/go/main.go?h=package+main

こうやって見るとビルドを含むgoのサブコマンドはCommand型で抽象化され、cmd.Runにて実行される。別プロセスになる訳でもなく、呼び出しているだけの模様。

go buildの本体はここにある。

https://golang.org/src/cmd/go/internal/work/build.go?h=base.Command

ディレクトリ構造がやはりgoのmainはディレクトリgoの直下にあり、その他は直下のinternalのさらに奥にある。これは他でも推奨されている構造だ。またやたらとディレクトリは多い。ディレクトリにgoファイルとtest.goファイルだけが1つづつあるというのが普通にある。これから考えてもgoはflatな構成をせず、package別に細かくディレクトリを細分し、下向きに掘り下げるのは意図的な設計な模様だ。

これまでの言語と異なり、こうやるものだと飲み込むべきだ。Javaが発表された当初も開発者の所属ドメインをpackage名の一部としてディレクトリとして掘り下げていくのは抵抗が大きかったが直ぐに慣れた。Goも直ぐに慣れるだろう。

海外のRailsから移った人が書かれたパッケージレイアウトのベストプラクティスでも似たようなことが書かれていた。プログラムのdomainとなる外部に非依存なデータ構造のみをトップに置き、外部依存が必要なものは依存別にpackageを作りディレクトリを掘って掘り下げていく。

Standard Package Layout – Ben Johnson – Medium

この方はpostgresqlやhttp等の依存する手段別にパッケージを分けている。その後、mockも別パッケージにし、mainで依存性の接続先を選ぶ(広義のdependency injection)ようにすることをお勧めしている。

このようなベストプラクティスはプロジェクトがでかくなり次第、必ず必要になるので良く読んでおくべきだろう。

エラー処理

GoBlogを古いほうから順に読んでいたらエラーの話もやはり多いのだが、Practical GoのerrWriterパターンをPike氏自身が書いていた。

Errors are values - The Go Blog

著名な日本のエンジニア、Jxckさんとの心温まるエピソードが良い :-)

errWriterパターンは無駄な実行が気になっていたのだけれどもそれについても一言書いてあっった。

There is one significant drawback to this approach, at least for some applications: there is no way to know how much of the processing completed before the error occurred. If that information is important, a more fine-grained approach is necessary. Often, though, an all-or-nothing check at the end is sufficient.

まぁ、大抵はこれで十分との話。

私は最初のerrorが出た後の空回りが気になっていたのだけど解決するとしたらどうするべきなのだろうか。実はこんな記事があった。

panicはともかくrecoverに使いどころはほとんどない - Qiita

この記事を書かれたUeyamaさんはGoogleでGoの開発にも参加されているスーパーエンジニアさんでこの方の書かれた記事は本当に深くて面白い記事が多い。

で、この記事はpanicに対するrecoverはほとんどの場合使う必要が無いとの主張だが、記事内ではGoの標準ライブラリ内でもどれだけ利用が少ないかという形で実際に利用されているソースが示されている。やはり深い実行スタックから一気に抜け出す場合が多い模様。

で、記事の趣旨に反してしまうのだけれども、errWriterパターンでこのpanic、recoverのペアで抜ける実装というのはどうだろうかと考えた。ワンチャンはあるかも。ただ微妙なのはpanicが出るのを忘れるとJavaのRuntimeExceptionを潰すのを忘れた時のように、実行時にエラー終了してしまう点だろう。

Goにon error goto文があれば良かったのにとか思った。ただGoではerrはあくまでvalueでしかないので実行系で区別するのが難しいか。*1

blockを越えられるgoto文、C言語のsetjmp/longjmp、Unixのsignal、いやいっそ例外に戻るか?悩みはつきない :-)

Goにはsignal handlerがあるんだなぁ。

https://golang.org/pkg/os/signal/

signalは基本panicに変換されるが、Notifyしておくとchannelに通知してくれる模様。それで気付いたのだけど、そもそもerrWriterをループするようなプログラムはgo funcにしておくと良いのか。で、errWriterがerrを見つけたらchannelで通知してmain threadは即終了と。errWriterを利用するgoroutineが終わらないけど。

errWriterのloop処理で、複数errWriterのwrite処理を呼ぶんだけれども、頭でやっぱりerrWriterにerrが出ているか一回くらいは見ればloopを抜けることができる。多分、loop数が大きいことが事前にわかっている場合にはそれくらいやるしかないのか。loop側でmainスレッドから終了通知をchanでもらっても同じ手間だし。

何か今一、コレだという納得感が無いまま終わる。 きちんとconcurrency patternsを覚えたら何か変わるだろうか?

goroutineは外から殺せないようだ。Javaも最初はThread.stop()があったのが何かの理由で無くなったのだっけか。何だっけ?

goroutineを外から殺せない理由はRussが次のように言っている。

https://groups.google.com/forum/#!msg/golang-nuts/SPAgm0TlWc0/SAbWyeBWMrEJ

"Killing individual goroutines is a very unstable thing to do: it's impossible to know what locks or other resources those goroutines had that still needed to be cleaned up for the program to continue running smoothly."

errWriterのloopをgoroutineにして、errが出たらpanicなりchannelで通知、mainスレッド側は殺せないのでチャンネルで通知なりするしかないと。以下のselectでchannelがcloseされると抜けるのが美しい。

How to kill a goroutine · YourBasic Go

ここまで書いてきたらどうもGoではこれらのためにcontextパッケージが用意されている模様。

https://golang.org/pkg/context/#example_WithCancel

ctxという物にはアレルギーがあるのだけれども標準ライブラリにあるのではしょうがない。郷に従えで飲み込むべきだ。

またerrWriterでは使えそうにないけど、そもそもチャンネルにはrangeが使用可能で、子のgoroutineでchannelをcloseすれば抜けるループが書ける。errWriterを回すループでchannelに進捗を報告し、errorが出たらクローズして抜けるようにするのもありか。

https://tour.golang.org/concurrency/4

まだまだGo力(ぢから)が足りない。精進せねば。

Goのerrorの柔軟性

何か文句ばかり付けているような文になってしまったが、Goのerrorがパッケージでそれぞれ拡張されていて目的に応じた柔軟なエラー対応が可能である点は利点として十分に意味がある。

Error handling and Go - The Go Blog

JSONやhttpでのエラー処理の説明。 ioやos.Openなんかでもfile.isExistなんかをエラーで処理できますね。 errorは文脈によっては出ても当然で対応可能な場合もあるし、そういう場合もerrorで判断が可能と。

*1:stringだけを持つstructなerrors.errorString型な場合が多いのだろうけど、この場合なら処理系はerrorを判別できるのだろうか。nil以外のerrorが返されたらerrorが発生したとして飛ばすon errorブロックとか作れるのだろうか。