Go言語感想文

最近、敵情視察を兼ねた仕事ととしてGoでアプリケーションを書いていた。このアプリケーションがどんなものかはそのうちid:tagomorisさんがどこかで話すと思うけれど、このコンポーネントOSS化される予定はいまのところないので、そこで得た知見をここにまとめておくことにする。

GoroutineとChannel

さて、GoといえばGoroutineとChannelですね。

Goroutineはようするにスレッドなんですが、文法と実装の支援でより気軽に使えるのが他の言語との違いでしょうか。なので、Goroutineをどれだけほいほい使うべきかというコスト感覚を身につけることがとても大事な気がします。Rubyなどとは気持ちを切り替えていく必要があるでしょう。ぼくはまだ切り替えきれていません。

もう一つがChannelですね。これは端的にはメッセージキューです。 Goは前述の通り同時に動くマルチスレッド (Simultaneous Multithreading; SMT) を気軽に扱えるわけですが、七つの人類悪の一つであるマルチスレッドに丸腰で立ち向かっても無残な死が見えています。 そのための道具の一つがキューで、適切にロックを用いて作られた通信路を経由してデータをコピーすることで、わかりやすくかつ比較的効率よくスレッドをまたいでデータをやりとりすることができるわけです。(ちなみにRubyにもThread::Queueってのがあったりしますが、CRubyだとGVLがあるのでわざわざ使うことは少ないかも)

とは言ってもそれなりにコストかかるんでしょう?って人はchansendの実装を眺めるとチャンネルごとのロックを取ってmemcpyしてるだけってわかるので、頑張って並列ハッシュとか使わなくていいんだって気分になれます。送受信にかかる時間は10~100ナノ秒くらいかな? とすると、話はいかに書きたいアプリケーションをGoroutineに分割し、間をChannelでつなぐかという話になります。そして、様々なデータは基本的にそれぞれのGoroutine内に閉じ込め、他のGoroutineはChannel経由でアクセスする。

この構図、どこかで見たことがありますね?そうマイクロサービスの話と同じです。 Goでアプリケーション書くのは、このようなマイクロサービス的なコンポーネント分割というマクロな楽しみと、Cのコードを触っている時のような細かなデータの取り回しというミクロな楽しみが隣り合わせにあって、独特の感覚を覚えます。

テストについて

Goの標準パッケージにはtestingってのがあります。このパッケージの思想はGo の Test に対する考え方などで解説されています。「まぁ、一理はあるけど……」という感想の方が多いのではないでしょうか。「いやダメでしょきつすぎ」って反射的に反応したくなりますが、敬意を表して実際にtestingでやってみました。

いや、ダメでしょこれ。結局テストのメッセージなんて”A is expected but B”が9割で、これのメッセージをいちいち考えるとか決断力の無駄遣いですよ。日本人が英語でメッセージを綴るリソースが有限だという悲しい現実を考慮していない。ぼくは5個くらいassertionを書いたところで嫌になってあとはひたすら”A is expected but B”をコピペしまくりました。

まぁでも、それで致命的につらいかというと真面目にやるのを放棄してコピペすれば良いし、Goのfmt.Printfは%vと%#vが便利なので、なんとかなりはします。assert_equalの類以外に、タイムアウトをつけてチャンネルを読んだりするassertionメソッドが欲しくなって作りたくなるかもしれませんが、そういうときはruntime.Callerで呼び出し元の行数が取れるので、うまいことやるとよいです。 まとめると、正直testingはダメだと思うけど、とはいえなんとかはなるのでtestingで依存先を減らすのはありかも。

その他

  • log.Fatalは使わない
    • カバレッジ上げづらくなる
    • fatalに追い込まれるようなエラーはもっと上流で捌くべきなんだと思う
  • テストでlogが邪魔なときはlog.SetOutput(ioutil.Discard)で黙らせる
    • 並列実行でうまくいかないのでloggerを持たせて個別にやった方がたぶんよい
  • 困ったときはgoto ← おまえほんとうにそれでいいのか
  • switchべんり ← おまえほんとうにそれでいいのか
    • selectやswitchの中でbreakすると外側のforまで届かないのでbreak <label>すればよいけど、結局goto使う
  • Printf(“%#v”, v)べんり
  • error伝播させたいときは、pkg/errorsのcause使う
  • とすると、エラー用構造体は作りまくった方がよい ← 本当に?
  • sync/atomicとかあるけど、new(expvar.Int)とかでmutex付き変数が簡単に作れる、便利
  • Goはnull安全ではない←構造体のポインタを扱い始めると気になってくる

まとめ

えーっと、この記事の趣旨は「Goは21世紀のRubyistが触っておくべき言語なのか?」でしたっけ。Elixirとかをやっていないのなら、Guildの予習として一度Goで何か作ってみるとよいと思いました。作ると言ってもこういう悪い例じゃなくてchannelを使ったアプリを作ってみると、従来のスレッドがうぇいうぇい暴れながら大事な共有リソースをロックで守るみたいなのとは違う設計が体に染みいります。みなさんもGoをいじってRuby 3.0を準備万端で迎えられるようにしましょう!

Re: Re: Go言語感想文

mattnさんのRe: Go言語感想文について。

テストについて

アサーションをコピーしなければならない理由は一つのテストケースの中で異なるテストが混在しているか、単に同様のテストがコピーして作られているのが原因ではないでしょうか。

あー、ちょっと言葉が悪かったかな。わたしはおそらくmattnさんより10倍くらいDRYに関して短気で、mattnさんの挙げた例で言えば、以下の3行ですら許せないのです。

        if !test.err && err != nil {
            t.Fatalf("should not be error for %v but:", test.input, err)
        }
        if test.err && err == nil {
            t.Fatalf("should be error for %v but not:", test.input)
        }
        if got != test.want {
            t.Fatalf("want %q, but %q:", test.want, got)
        }

あとは、Table Driven Testsって基本的には関数的というか入力と出力が対応するものでは便利なんだけど、REST APIを叩くコードのリトライ部分のテストとか、状態を持って他のコンポーネントとchannelで通信しながらループするコンポーネントのテストにはいまいち。

その他

Golang に限らずですが、最近の言語では Labeled Break という物があります。

あ、ゴメン「selectやswitchの中でbreakすると外側のforまで届かないのでbreak <label>すればよいけど、結局goto使う」の<label>がタグ扱いになって消えてた……。それはそれとして、上述のリトライみたいなケースだと結局goto使いました。

null安全について

これは世間一般での意味での「null安全」です。ポインタではなく直接構造体を扱えばうまいこと行くかなーと思いきや、なんやかんやでポインタ使う羽目になったりして悲しい。