そろそろGoについて一言いっておくか

Play on Words

昨日、GoCon(ごうこん)なるイベントに参加してきた。以下に続く話は5割以上がフィクションなので虚実織り混ざっている様を楽しみながらお読みいただけたらと思う。

最初に発表されたニュースを聞いたときは、Goはよい車輪のよい再発明で、結局GoogleC++Javaを使い続けるだろうし、世間はGoogle独自言語としてみなすのだろうなという予感はあったし、2010年だから2011年ころはそういう見方をされていたように記憶されている。私もそういうものだと思っていたし、特に関心を持つこともしなかった。いま思えば正常性バイアスだったのだろう。

実際に昨日のカンファレンスで一番興味深かったのは鵜飼さんによるGoの解説だった。比較対象がC++, Python, Javaだったことが最も印象的で、普段からErlangOCamlといった関数型言語に接していた身として新鮮だった。話を聞くうちにGoogleの本気度が分かってきて、これまでの言語では巨大なサーバーシステムを0から構築するためには不十分であったり、やや行き過ぎたところをバランスをとって…と、そういった問題点をひとつひとつ解決していきたいという意図が見えた。つまり、

  • メソッドやメンバを追加できないのは不便だがダックタイピングはメンテナンス性が非常に悪い
  • コンパイル時間が長いのは地獄
  • メモリ管理は地獄
  • 言語仕様がシンプルなのがいい、難しいことはやりたくない
  • シンプルでいてある程度は型安全なプログラミングがしたい

といったところで、最後の汎用言語を作りたいのだろう。これといって尖ったところもなく、言語仕様の設計がとてもバランスがとれていてよい。実際に検索エンジンのバックエンドはC++Javaで書かれているだろうし、巨大なコードベースがあっていくら天才たちとはいえそのメンテナンスに苦労しているのだろう。他のex-Googlerの講演で数時間かかってビルドしたバイナリが数GBあったという笑えないフィクションの話も非常に興味深い。
Tech Conferenceなのであまり相応しくはないだろうが、おそらくJavaもオラ◯ルさんの手中にあってどうなるか分からない*1し、gccもどうなるか分からない*2し、まあLinuxはしばらく大丈夫だろうけどもというくらいの妄想は僕にも働く。

ところで、ここからはフィクションの話だが、ある知り合いが巨大な分散システムの開発をしていたことがあった。そのシステムはC, C++, Rubyが入り交じっており*3、コードはとりあえず書けたものの、オブジェクトのシリアライズやマルチスレッドプログラミングに伴うロック管理、メモリ管理に多くのコストを費やした。複数リソースのタイムアウトや受信などを待ち合わせるコードをCで実装し、さらにメモリ安全性も保証しなければならない。ここまでくるとほぼ組み込み系のほぼリアルタイムシステム*4のプログラミングになり、もともとそういうノウハウを持ってなかったこともあり荷が重かった。さらには複数のコンピュータが特殊なプロトコルで協調して動作しながら、いつ故障してもデータの整合性を保たなければならないという古くて新しい難しい問題も同時に解かなければならないし、分散システム特有のカスケード故障、多重同時故障も想定しなければならず、デバッグやテスト、特に長期安定試験や性能試験の際に大きな苦労をした。
クリティカルなバグが見つかると、コードの大部分を再度レビューにかけて類似の問題点がないかを時間をかけて検討することもあった。修正範囲が大きくならざるを得ないときは本質的な解決を諦めてコードにさらに泥を塗って汚したこともあった。だから、Goのように筋のよい言語があればもっとやりようがあったのかもしれないと思う。
Concurrency Bugは大抵Segmentation Faultかカスケード故障、またはシステム上重要なメッセージ通知の喪失として現れてくる。複数の種類の問題が複雑にからみ合って登場してくるのだ。Rubyに至っては1.8系を利用していた*5ので、グリーンスレッドとpthreadとGCには嫌でも詳しくなれたことだろう。どうしてRubyにforkというAPIがあるんだと恨んだりもしたことだろう。
当時の担当者は「これがErlang/OTPなら…」と何度思ったか知らないと語っていたが、(主に人数と費用が)大規模な開発に特有の問題として、有名でない言語・処理系は採用しにくいということもあり悶々としていたという。ちなみに当時はまだGo言語が発表されておらず、「これがGoなら…」と思うことはなかったし、Goの発表を聞いても、いくら筋がよくても未熟で普及していない言語なので採用されることはなかっただろう。

しかし、当時、Goの1.1がstableになっていたら…と思うと非常に残念な気持ちにもなるし、なかったものは仕方ないのだと諦めもついている。

僕自身は、libcへの依存を排除して全て静的リンクするところが一番気に入った。簡単なので実際に試してみてほしいのだが(これはMacの例なのでLinuxならotool -Lの代わりにlddとすればよい)、

$ go build hello.go
$ otool -L hello
hello:
$

という風に、依存ライブラリが全くない。ここから分かるのは、Goでライブラリの依存性で困ることは全くないということだ*6RubyPythonなのでもたまに依存ライブラリで苦労することはあるだろうが、CやC++の動的リンクに比べたら全く大したことない。
もうひとつよかったのは例外がないということだ。Cはデフォルトで例外安全な言語だ。それだけの理由でC++を嫌う人もいるし、多くの規約が例外の使用を禁止している。panic / recover の処理をdeferで先に設定できるというのはこれまでになかった発想で、例外処理を分離できてよいのかもしれない。個人的にはクロージャは便利だけど、コンテキストやスタックの内容がわかりにくくなるので好きじゃない…

気になるのはネットワークやディスクのI/Oまわりの実装だ。せっかくgoroutineがあるのだから、goroutineと非同期I/Oを組み合わせて同期I/Oっぽいプログラミングができるようにしたらよいと思うのだが、@methaneの話を聞いているとどうもI/O待ちをしている間はPは手放すけどネイティブスレッドは一人捕まえておくようになっているらしい。それだとI/O待ちをしているgoroutineが1000とか10000いたときにどうにもならないと思うのだが、もうちょっと調べた方がよいだろう。
あとはGCか。グローバルに共有できる変数を宣言できるしMutexなどの並行プリミティブがあるのでどういう挙動を示すのかよくわかっていない(勉強が必要)。メモリ空間を多くのgoroutineで共有したときにGCが終わらない問題が出るだろう(それがDerekがスタックを推していた理由でもある)。

しかし型宣言を後置でつけるのはどうしても理解できない。前につけたらいいじゃないか。テンプレート型がないのはC++で苦労したからなのかもしれないが、やっぱり僕はこんな風に型を記述したい*7:

fold_left :: (fun a b -> c) [a] b -> c

したいです。あとは、goroutine supervision treeをつくるために spawn_link という関数を用意すべきだ。

まとめると、マルチコアを使いつつネイティブコードで動作させたくて、かつ金融的なデリバティブ評価関数を書くのでなければGoを使えということだ。

*1:そのためのDalvik VMなのだろうが、そもそもJVMJavaも…

*2:FSFって、僕のような人間からすると実態がよくわからない、かといってLLVMってAppleの息がかかっているような気もする

*3:幸運なことにみんなJavaが嫌いだった

*4:この「リアルタイム」はクラシックな意味の方です

*5:既に動いているシステムは偉大だ

*6:組み込みではまだサイズが大きくて困るケースもあるだろう

*7:どこのソースを見たら