Go言語の不満

ちょっとバイナリ配布したいツール↓があったので、Go言語と戯れました。

zenn.dev

ほぼはじめてGoを使ったので、にわかほど語りたがる法則に従って、Go言語の感想を書きます。

新しい言語にふれたときは、できることには気づきにくく、できないことに気づきやすいので、不満が多めです。主な比較対象はRubyC言語、JS/TS、Rustあたりです。

よかったところ

  • ひとことで言えば「便利になったC言語」という感じでした。結構低レベルなAPIも揃っていてよかった(デーモン化が素直にできなかったこと以外)。
  • Rustと比べたらストレスフリーです。思った通りに書くだけでとりあえず動いてくれる。すばらしい。
  • 見た目はあきらかに長くてダサいですが、こだわりを捨てて割り切って書けると言えなくもない。
  • 配布しやすいシングルバイナリが作れるのはやはりよい。今回Goを選んだ理由がこれ。

細かいカプセル化がむずかしい

カプセル化の単位がパッケージしかないのがとにかくつらいです。ローカル変数以外の変数や構造体のフィールドが、パッケージ内のどこからも参照できてしまう。

今回はじめて自覚したのですが、ぼくは数十行単位の細かい抽象化を積み上げてプログラムを書くようです。たとえばRubyだと、インスタンス変数は基本的にclass~endの中でしかアクセスできません。これにより、単一ファイル内でも細かくカプセル化ができます。

同じことをGoでやるには、パッケージ(つまりディレクトリ)を分けないといけないので、数十行のファイルが一つだけあるディレクトリを大量に作るハメになりそうです。そんなの明らかにやりたくないし、Go的に推奨されてもなさそうです。

別案として、すべてのstructをinterfaceにキャストして扱う手も教わりましたが、それは明らかに面倒くさすぎる(わがまま)。

慣れの問題もあると思いますが、同一パッケージ内でもファイル単位などで手軽にカプセル化したい。この点はextern宣言やヘッダファイルを#includeするC言語のほうがマシとすら思った。

goroutineむずかしい

むずかしくないですか? wsl2-ssh-agentでは、複数のsshクライアントからのリクエストを受けて、それらをシーケンシャルに処理するサーバを書く必要があったのですが、設計に数時間以上かかりました。

とりあえず動かすだけなら10分くらいでできたんですが、雑に立ち上げたgoroutineをすべてきちんと終了させるのがめちゃくちゃしんどい。雑にチャンネルをクローズすると、それに書き込むgoroutineがpanicする可能性が(稀なレースコンディションとして)見つかったりするので、終了させるべき順序がめちゃくちゃ繊細です。今もバグが取り切れた自信はないです。

Goではcontextが便利という噂を聞いてましたが、IO待ちがcontextを受け取ってくれないので、肝心なところで使えない印象でした。

この問題は他言語なら楽というわけでもないですが、今回の問題に限って言えば、シングルスレッドでselect(2)みたいなAPIのほうが楽そうだったなあと思いました。Goを捨ててC言語で書き直すか迷ったくらい。「goroutineをすべてきちんと終了させる」なんて考えないほうがいいんですかね。

その他雑な感想

  • そうは言ってもやっぱ記述が冗長すぎる。if err != nil {} つらい。サンプルコードの読解すらつらい。
  • めっちゃ書かされるわりには型が弱いので、なんとなく割に合ってない気分。nil unsafeとかは別にいいんだけど、組み込みでoptional intくらい欲しい。
  • めっちゃ書かされるわりにはエラーの扱いが微妙? エラーのとき、どこで何が起きたか把握するのに手間がかかると思うことが多かった。スタックトレースで見たい。
  • テストむずかしい。os.Exit(0)を呼ぶ関数はほぼテスト不能とか *1動的言語のテスタビリティの高さをあらためて感じた。

*1:os.Exit を変数経由で呼ぶようにしておいてテスト時に差し替えるみたいなテクニックもあるようですが、コードにとって完全に無意味な複雑性をテストのためだけに入れるのはちょっとなー。