#golang は大規模環境向けか?
この記事は2021年5月頃に Scraps で書きなぐった記事をいちから書き直したものである。 Scraps の記事は既に非公開(アーカイブ)化している。
4,5月頃のこの手の記事(ポエム)は季節の風物詩みたいなもので,夏休み真っ盛りに書くには季節外れかもしれないが, Twitter でエゴサーチしてて「ひょっとしてブログ記事として纒めておいたほうがええかしらん」と思ったので,今更ながら書いておく。
起点となる tweet は以下:
ちなみに表題の問いに対しては yes とも no とも言えない。リアルで問われても,おそらくは「時と場合による」としか答えられない。ソフトウェアというのは基本的に「一品もの」で,一般論で大雑把な話はできても,詳細を掘っていくほど難しくなる。プログラミング言語の選定はその最たるもので,結局は手持ちのソフトウェア資産と動員できるエンジニアのスキルで決定することが多い。まぁ,手持ちの使い回しばかりでは先細りするだけだが。はっ,今の日本の話か(笑)
要件定義で言語を選べる自由があるというのは(プログラマにとって)幸せな部類だろう。
メモリ管理と並列処理の抽象化
Go の分かりやすい特徴は「メモリ管理および並列処理のコントロールは言語仕様およびランタイム・モジュールに組み込まれている」点だろう。
たとえば,ある関数内で宣言されるローカル変数の実体(インスタンス)がスタックに積まれるのかヒープに生成されるのかプログラマが気にする必要がない。
var global *int
func f() {
var x int
x = 1
global = &x
}
と書いたときのローカル変数 x
のインスタンスはおそらくヒープ上に生成されるし(関数スコープの外で参照されるため)
func g() {
y := new(int)
*y = 1
}
と書いたときのローカル変数 y
のインスタンスはスタックに積まれているかもしれない(もしくは最適化で何もなかったことにされるかw)。インスタンスがヒープ上に生成される場合でも,その解放タイミングは GC (Garbage Collector) が決定する。
プログラマが気にするのはインスタンスの参照範囲と期間を最小にすることくらいで,あとはコンパイラや GC に丸投げできる。ちなみに GC のパフォーマンスについては 1.9 の時点で
The runtime.ReadMemStats function now takes less than 100µs even for very large heaps.
と主張している。
並列処理(parallel processing)についても Go では goroutine によって「並行処理(concurrency)」を抽象化しているため,それがどの CPU コアのどの CPU スレッドのどの OS スレッドで動作しているか気にする必要がない[1]。 Go のランタイム・モジュールは,ある goroutine に対して通常 OS スレッドよりも更に小さい単位の独自スレッド[2] を生成し,独自のスケジューラで管理している。このためハードウェアや OS の機能を超えて大量の goroutine を生成・駆動させることができる。
こうした観点から見れば,確かに「局所的な最適化でバグや脆弱性を生み出すくらいなら,足りない性能はプラットフォームをスケールアップさせればいいぢゃない」といった感じの「富豪的」発想が透けて見えるが,だからといって「CPUやメモリがある程度潤沢である程度大きな規模な環境での開発に対応する目的で作られて」いるかというと,因果関係が微妙に違うと思う。
API の抽象化と POSIX 依存
最近面白いと思った記事が
である。 Go の特徴のひとつにマルチプラットフォーム対応があると思うが,この特徴はこうした API の抽象化による恩恵であると言えるだろう。特に io.Reader/Writer インタフェースによる入出力の抽象化は本当によくできてると思う。
しかも Go の標準パッケージは単なる抽象化ではなく,その辺の汎用フレームワーク製品と比べても遜色ないレベルであり,たとえば Web アプリケーションを組む場合でも標準パッケージのみでかなりのことができてしまう。
一方で,これら API の抽象化は POSIX 互換システムを前提に設計・実装されている。したがって POSIX から大きく外れるプラットフォームでは使いにくいという欠点がある。
ぶっちゃけて言うなら Go でカーネルやデバドラそのものを組むのは無理である。もっと言うなら要件の厳しいリアルタイム処理[3] にも向かない。そういうのは Rust とかに任せましょう[4](笑)
TinyGo があるじゃない
本家 Go が POSIX に依存しているというのなら, POSIX への依存度が小さいツールチェーンを作ればいいじゃない! ということで考えられたのが TinyGo である。
TinyGo は LLVM ベースの組み込み用途向け Go コンパイラで,流行りの WebAssembly とも相性がいい。本家 Go コンパイラに比べてかなり小さいバイナリを生成できるのも特徴だが,その代わり使える機能についてはいくつか制限がある。
TinyGo を使うことで,いわゆる IoT 向けの小さなシステム開発を行うことができる。
はやく作って はやく改(なお)す
Go はコンパイル言語には珍しく(と言っていいのか分からないが)「はやく作る」ことに特化した言語と言っていい。「はやく作る」というのは事前学習量やコンパイル速度やコード記述量を指しているのではなく「考えたことをそのまま書いて安全に組めるか」ということだ。 Go の言語仕様に組み込まれた簡便さも制約もこの目的のためにあると言ってよい。
たとえば Go は Java などの伝統的オブジェクト指向プログラミング言語とよく比較されるが,例外処理や継承など「考えたことをそのまま書く」ことにおいてノイズにしかならないギミックをあっさり捨て去っている。 Goroutine 間に優先順位が存在しないのも sync.Mutex が再入不可なのもちゃんと理由があるのだ。
もうひとつの Go の特徴はリファクタリングに厚い言語であるということだ。シンプルな言語仕様故に手を入れやすいし, interface 型を使ってオブジェクト間の関係を「疎」にできるため,関係を組み替えたり,再利用性の高い機能を別パッケージとして切り離したり,なんなら「出来のよくないパッケージを丸ごと入れ替える」なんてことも比較的容易だったりする。
一言で言うなら「はやく作って はやく改(なお)せる[5]」のが Go 最大のメリットだと私は思う。実際に Go は CI (Continuous Integration) や CD (Continuous Delivery) といったものとの相性がいい。
これから作ろうとするシステムで refactorable であることを要件とするなら Go を候補に入れるというのは悪くないと思う。
参考
-
Go で並列処理の詳細を気にしなくていいということは,言い方を変えると, Go の並行処理で安直な時分割処理を想像するとバグの元になるので止めたほうがいい。 ↩︎
-
Go が goroutine 単位で管理する独自スレッドはスタックサイズで2KB程度の小さなサイズで生成され(必要に応じて)動的にリサイズされる。更に 1.14 以降はプリエンプティブ・マルチタスクでの動作が可能になった。 ↩︎
-
ここでいうリアルタイム処理とは「分割されたジョブを決められたタイミングで決められた期間内に完了すること」を指す。 ↩︎
-
Go と比較するなら Rust はランタイム・モジュールの機能(というか責務)を最小にすることでコードの自由度を上げている。故に Rust はカーネルやデバドラの開発にも向いている,というわけだ。ただ「コードの自由度が高い」ということはコードを書く側に責務が寄せられているということでもある。安全かつ refactorable でパフォーマンスのよいシステムを組めるかどうかはプログラマの技量による,と言い換えてもいいだろう。 ↩︎
-
「改す」を「なおす」と読むのは辞書的に正しい日本語ではありません。念のため(笑) ↩︎
Discussion