「Go っていいの?」への応え
Go Conference 2017 Spring で Lightening Talk で話し足りなかったことを書いておく。
Go で書いたと話すと「Go っていいの?」と聞かれることがある。まともに伝えたい相手であれば、この質問には直接答えずに「どんな課題があって、Go はどのようにその解決に役立ったのか」を答えている。
要素技術の選択には、かならず文脈が影響する。Matz がウェブアプリを作るのと、私がウェブアプリを作るのでは、そもそもの前提が大きく違う。あと、スキルが低い奴の話なんて、みたいな言われ方をされることもある。それはまあ正しいんだけど、そのスキルアップに1年かけてたら預金がなくなってしまうのだ。いや、半年ももたないか。いずれにしても、現時点でのスキルをスタート地点として、納期までに成果物を仕上げて、現金を獲得する必要がある。そういうのも文脈のひとつだ。
私は Python で Web アプリ/サービスのサーバーサイドの開発をしてきた。あとは管理画面を Angular で書いいたり、PoC (Proof of Concept; 概念実証)的に Swift/iOS、Java/Android を書いたり、程度。
要件
- Windows 上で、特定の DLL を呼び出すプログラムを開発する
- そのプログラムと、顧客が書いたプログラムが、まとまった処理ごとにデータを交換する
並行実行できればいいという話で始まったのだけれど、私が書いたプログラムと、顧客が書くプログラムが、同期してデータを交換することになった。2つ以上のプログラムが、1台以上のPCで稼働して、同期しながらデータを交換する。
個々のプログラムどうしでデータ交換させるとややこしいので、経由するサーバーを作ることにした。メッセージのブローカーというか、KVS というか。リクエスト/レスポンスで通信するにせよ、pub/sub にせよ、レスポンスをブロックする必要があるなと思った。また、顧客のプログラムもサーバーにアクセスするので、プロトコルや挙動をシンプルにしたかった。サポートが大変だからだ。ソケットの send と recv だけで動かして、recv をブロックしたいなと。
あとこれらのプログラムは研究開発用途のコンピューターで動かす。どんな構成になっているか分からないし、直接アクセスできないところに置かれてそうだったので、ランタイムや OS に厳しい条件をつけたくなかった。
ランタイムへの依存を減らしつつ、並行/同期しやすい開発ツール
どんな言語でも、シングルバイナリにする方法はある。py2exe を使えば、python.dll も含めてひとつのファイルにできる。できるんだけど、Go はそもそも公式のツールが、いきなりバイナリを吐き出すことが前提になっている。
Go には goroutine とチャンネルという言語レベルでの並行/同期のしくみを持っている。サーバーでブロックするつもりだったので、内部で接続を複数保持して、必要になったら返すみたいなことが必要になる。LT では複数のプロセスを立ち上げることを concurrency のように話したけど、本当に concurrency が必要だったのは、サーバー側で接続を保持して処理するところだった。そして同期させる必要がある。
Go 言語については「(マスコットキャラも含めて)いろいろとキモいこともあるけど、慣れてくるとかわいい」と、鵜飼氏が言ってた気がする。同じ印象を持っている。
静的型付け
Go に限らないんだけど、コンパイル時に型チェックがあると「ここから適切な型が返ってくるか?」ということに神経を使わなくていい。interface{} を返したりするとアレだけど。
オブジェクト指向とかクラスとか
クラスのインスタンスのメソッドを呼び出す、っていう記述モデルではない。Java のようなクラス的な記述ができるといいなとは思うけど、そんなに大きなつまづきには、私はならなかった。たぶん Python の経験があるからだと思う。
Python では
class Foo:
..def __init__(self, prefix): # . はスペースだと思って!
….self.prefix = prefix
..def hello(self, text):
….print(self.prefix, text)foo = Foo(“chinko”)
foo.hello(“show”)
みたいに書くんだけど、この self の明示が Pythonic である。foo.hello(“show”) は、 Foo_hello(foo, “show”) の syntax sugar なのである。
Go では
type Foo struct {
prefix string
}func (f *Foo) Hello (text string) {
fmt.Println(f.prefix, text)
}f := Foo{“chinko”}
f.Hello(“show”)
似てる。すごく似てる。引数を書く場所が違うだけだ。Go を使うにあたって、現時点で「私が」持っているメンタルモデルを、大きく変更する必要がなかった。
API とか、C で書かれたライブラリで、なんかのリソースを管理するときって、たいてい第一引数にその対象へのポインタや、ポインタのポインタを指定する。Win32 API とか。たぶん Java とか Ruby とかも、内部では似たような事やってんじゃないんすかね。違うんすかね。そういう内部構造を隠蔽するために、いろんなプログラミング言語があるんだけど。
goroutine とチャンネルが役に立つ
ウェブ API を実装するときのプロセスやスレッドの管理は、gunicorn とか Google App Engine とか AWS Bean Stalk にまかせてしまうんで、あんまり考えたことがない。
Python なんかでちゃんと管理したかったら、タスクとかスレッドとかプロセスとか、まあなんでもいいんだけど実行するべき処理と、その状態を管理するオブジェクトに隠蔽するようなライブラリを使うことになると思う。JavaScript だときっと promise を連結していく感じになるんであろう。
Go では、go f() って書いたら、それだけで並行ルーチンで実行される。そうそうこういうのでいいんだよ。
その代わり goroutine は分離された関数実行なので、それを制御するようなメソッドとかはない。代わりにチャンネルを使ってメッセージパッシングする。チャンネルはブロックされるので、同期もできる。関係ないけど、私は語尾の L の音が聞き取れないので「ちゃんねー」って聞こえる。はー、ちゃんねーと飲みたいですね。
goroutine + chan はテストを書くときも便利だった。他のプログラムを起動するとか、サーバーにリクエスト送って、ブロックされたレスポンスを待つときに、
go func(ch chan int) {
ch <- doLongTask()
}()if result := <-ch; result != expected {
t.Errorf(“result is %#v, want %#v”, result, expected)
}
みたいに書ける。
まとめ
そんなわけで、簡単ではなかったけれど、困難を極めるというほどでもなかった。Go だったから、このくらいのしんどさで落ち着いたんだろうなと思う。
公式ドキュメント以外では「プログラミング言語 Go 」と「みんなの Go 言語 」を参考にした。プログラミング言語 Go は、Go のファンダメンタルな考え方なんかを参考にした。たとえば、チャンネルを読み込んで処理する無限ループを持つ goroutine を作れば、それがひとつのなんというかアクターというか、サーバーというか、そういうのを作れるよとか。
みんなの Go 言語のほうは、もうちょい実践的なことが書かれている。done チャンネルや context.Done()で goroutine を止めるとか、コマンドラインツールを書くときのディレクトリ構成とか。あと DLL 呼び出しについては mattn のブログばっかり読んでた。