🦜

Goの苦手な領域

2021/09/19に公開
4

Goの利点を使って実装するコツやノウハウを書くことがコミュニティにとってプラスになると思っているのでそれに専念したいという考えはありますが、Goの苦手な領域にGoを採用してしまってヘイトを溜め込んでしまう事例を見かけたりします。

こういう悲劇の起こる可能性を少しでも減らせたらという思いで、Goの現状の苦手な領域について解説しようと思います。Goを学び始めにこれらの領域に手を出すのは避けましょう。

Cgo is not Go

http://go-proverbs.github.io

GoはCGO連携でC/C++資産を利用することができますが、メモリアロケータの異なる処理系を繋ぐ関係上、お互いに呼び合う際のパラメータや戻り値はほとんどのケースでコピーが必要になります(Cの型でメモリ確保しCの型のまま受け渡しする場合はOK)。なので高頻度に呼び合うような用途には不向きであるというのはSWIGなどのような複数の処理系を連携させる仕組みと同様です。

また、Goの周辺ツールや依存解決のエコシステムには一部分しか載せられません。C/C++の依存解決は別途必要になります。

CGOが絡んでしまうと、Goの利点のいくつか(ビルドの速さやクロスプラットフォーム対応など)が欠けてしまい、開発体験が低下します。なのでGoのライブラリの多くはC/C++依存を切り離そうという動きがあります。(Javaが既存の資産をJavaで再実装してきたのと同様に)

例えば

- OpenSSLに関する機能はほぼほぼGoで書かれていて標準ライブラリに含まれています。
- WebRTC関連機能をGoで再実装したpion/webrtc
- SQLite3をPureGoに変換したmodernc.org/sqlite
- cairo(ベクターグラフィックスライブラリ)ライクなPureGo実装canvas

などがあります。

もちろん、機能要件を満たすのにCGOを使うことは何も問題はありませんが、いくらかのデメリットを受け入れる必要があります。

Goはビルトインのツールチェインだけで同じソースコードから各種プラットフォーム向けのバイナリを簡単にビルドできるという強みがありますが、CGOを使った時点でこの強みは半減します。

ダイナミックリンクライブラリを他の処理系に提供

そもそもWindowsではまだプラグインモードビルドする方法が確立されていません。
また、goroutineやGCのためのランタイムがついて回るため、ダイナミックリンクライブラリをロードするごとにメモリ利用率がかさみます。多数のプラグインをロードするためのコンポーネントとしてはGoはあまり向いていません。
しかも前項の通り、メモリアロケータの異なる処理系を連携させる場合、受け渡しするパラメータのほとんどをコピーする必要があります。

受け渡しする内容を常にコピーするのであればRPCスタイルにする方が開発体験としては良くなることが多いです。なのでGoのプラグインシステムで実用されているものはRPCスタイルで各プラグインが別プロセスになっているというものが多いです。

システムプログラミング

https://golang.org/ref/spec#Introduction

ここの一文に「Go is a general-purpose language designed with systems programming in mind.」と書いてはあるのですが。

ここでいうシステムプログラミングはハードウェアとソフトウェアが設定された目的のために協働するというような広義の意味を指しています。

しかし、「狭義のシステムプログラミング」はOSのないところやOSとの境界で動作するようなプログラミングを指していたりしますが、こちらはGCのある処理系は入り込みにくい分野でもありますし、Goはgoroutineランタイムが必須なのでOSの支援のない側の実装には不向きです。

GoでOSを実装し、GoでそのOS用アプリケーションを書くという実験実装がCで実装されたものの9割程度のパフォーマンスを出したという成果はありますが、特にメモリアロケータの異なる処理系と協調して動かすにはいろんな課題が残っていて実用に至ることはなかなか難しいようです。

つまり、Goの用途としてシステムプログラミングというのはOSの上で組むという前提の広義の意味なので、「狭義のシステムプログラミング」にGoを採用しない様にしましょう。

組み込み開発分野

Goの紹介には組み込み開発分野にも向いているというような言説がありますが、「組み込み開発」は2つに分類できます。

  • 「PC-OSの動くプラットフォームをターゲットにした組み込み開発」
  • 「PC-OSの無いプラットフォームをターゲットにした組み込み開発」

Goはもちろん前者にしかマッチしません。後者にマッチするためのTinyGoというサブセット処理系はあります。ここは使い分けが必要な分野になります。

Railsがカバーする分野

Goの良さの源泉がその哲学にある様に、Railsの良さの源泉もまたその哲学にあります。Goに形だけRailsを目指して作られたWebフレームワークが実装されても(いくつかそういう実装は作られましたが)結局Railsの代わりにはなかなかなれないのです。

言語仕様のギャップがあることも関係しているし、Railsがその間に進化して引き離していくということもあります。Railsのような実装が欲しい時に、Go側のRails風フレームワークのエコシステムが充実するのを待つより先にRailsで実装した方が早いというのは事実でしょう。(Railsを求めているのなら素直にRailsを使おうというのはGoに限らないことですが)

また、GoがRails風味を取り込んでいくとGoの旨味の一部(型安全性やパフォーマンス面など)が失われるという側面があります。なのでGoにRailsを求めていくとしんどくなるのはこういうところです(Go2のジェネリクスが導入されればこの側面が解消されていく可能性はありますが)。Go製バックエンドに求められている重要な要素は型安全性やパフォーマンスなのであまり積極的にRails風味を取り込んでいかないのです。たとえGoの旨みを捨てつつRailsっぽいものを実現したとしてもRailsに比べ期待した性能が大して発揮されないということも十分に起こり得ます。

旧来のHTML生成ー>ブラウザに送るー>サブミットされたフォームを受け取りー>バリデーション結果によりエラーメッセージを含むHTML生成&表示かリダイレクトするかなどブラウザ側の表示をサーバーサイドからリモート制御しつつハンドリングするというようなスタイルがありますが、フロントエンドとバックエンドの関係性が密になってしまうためにこのスタイルはだんだん採用されなくなってきています。

フロントエンドとバックエンドを疎結合に保つためバックエンドはAPIだけを提供し、フロントエンドとはAPIを通じてバックエンドと連携するという、あくまでブラウザの表示を制御するのはフロントエンドの役割とするスタイルが現代では主流になりつつあります。

後者のスタイルならば、Goは得意ですし、そのためのライブラリも充実してきています。前者のスタイルをGoで実装するのは結構骨が折れると思います。(ただし、シンプルなものに留めるならありだと思いますし、ここを開拓しようという人もいるとは思いますが。)

なので、前者のような旧来のHTMLをサーバーサイドで意識しなくてはならないようなWebアプリケーションにGoを採用するのはあまりお勧めしません。

追記: MVC Scaffolding対応バッテリーインクルードなフレームワークとしてBaffaloというのがあるよという情報をいただきました。DjangoやRailsのようなものをGoでやってみたい人はチャレンジしてみてください。

Windows向けのファイル検査ツール

https://zenn.dev/nobonobo/articles/5b1872497502d5

この記事に書いた通り、WindowsAPIの呼び出しにはいくらか曖昧さが含まれます。なので異常系のファイルパスを整理したり修正したりするようなシステムメンテナンス系のツールを作るにはWindowsAPIを直接呼ぶなどの特殊なアプローチが必要になります。

WASMでライブラリ提供

WASMによるライブラリ提供は前述の「ライブラリを他の処理系に提供」が苦手なことがあり、WASMのインスタンスが機能提供をするような用途に使いにくいという点があります。もちろんグローバルな名前空間に機能提供用の関数を差し込んでいくというスマートじゃ無い方法で対応することもできますが、まぁあまりお勧めはできません。また、WASMバイナリサイズが他の処理系と比較して大きすぎるという欠点もあります。サイズを許容できるようなアプリケーション用途(例えば最初のロードが長いゲームなど)やWeb経由の転送が必要ない環境での利用でなら採用しても良いとは思います。

この辺りはTinyGoの方が一歩進んでいます。Goとのコンパチビリティは後退してしまいますが、シンボルのエクスポートが可能になってきていますし、WASMバイナリサイズもGoの5分の1程度にまで小さくなることが期待できます。

高メモリスループットなもの

GoのGCはレイテンシー重視のアルゴリズムを採用していて、通常はストップする時間を短くする方向に向いていますが、大量のメモリ確保と破棄を繰り返すような高いスループットを要求された場合に大きくストップする時間が挟まれるということがありました。

Tips: しかしこれはGoの1.12あたりに大きなスループット改善があってから発生しにくくはなっているはずです!またこの問題の解決方法としてバッファをプールすることでGCの負荷を下げるというアプローチを採るのがオススメです。

シングルコア限界性能を必要とするもの

観測上、CPUアーキテクチャ依存な最適化やSIMD対応ライブラリなどは優先順位が低めに見えます(クロスプラットフォームサポートを優先している?)。なのでCPUヘビーな処理であと数%でも引き上げたいというようなところには向かないです。僕のこれまでの観測ではGoはC/C++やRustの90%ぐらいのシングルコア性能くらいの認識です。あと10%を捻り出すことに価値のある分野には向いていないと思います。

まとめ

  • ここにある内容が周知され、悲劇が繰り返されないことを望みます。
  • Goはやはりプロセスを起こすようなアプリケーションに向いています。
  • じゃあGoは用途が狭いのかというと一応システムプログラミングのところに書いた通り「general-purpose」ではあります。ここに書いた条件に当たる場合のみシンドイということです。
  • 他に思いついたら追記します。指摘もお待ちしています。

Discussion

NoboNoboNoboNobo

誤解されると困るのですが、ここにある条件に当てはまっていても要件を満たすのに必要でデメリットを飲めるなら他のメリットを期待して採用してもいいわけです。また、パイオニアになるつもりでもOKなのです。
ここに当てはまるものに使っちゃダメということではなくてここに当てはまると苦労が増えますよということです。

NoboNoboNoboNobo

反応に多く言及されている「CGO+alpine」でトラブっている話はほとんどのケースでちゃんと回避策はあります。コミュニティに疑問をぶつけてみてください。(ほんの少しの方針変更は発生するかもしれませんが)

NoboNoboNoboNobo

GCに関する反応が多いので補足しますが、GC負荷を下げるテクニックは存在します。
面倒なところはGCの恩恵を受けつつ、クリティカルなところでこれらのテクニックを駆使して対処することは可能です。特にオーディオプロセッシングなどはバッファの使いまわしを意識した設計にすることで十分に対応可能です。