世界を動かす技術を、日本語で。

「Go」はまだ良くない

2025年8月22日原文(blog.habets.se)

概要

Go言語に対する長年の批判点を整理。 エラー変数のスコープやnilの扱いなど設計上の問題指摘。 移植性やリソース管理の難しさにも言及。 例外処理やUTF-8前提の文字列仕様の危険性を説明。 他の言語・実装との比較を通じてGoの課題を強調。

Go言語設計に対する批判点

  • Go言語 の設計には、既存の知見を無視した 不合理な点 が多い。
  • 過去記事 でも指摘した内容の繰り返しになるが、依然として根深い問題。
  • 歴史的経験 があるにも関わらず、Goは安易な実装に終始した言語設計。

エラー変数のスコープ問題

  • エラー変数(err) のスコープが 広すぎる ため、可読性と安全性が低下。
  • if文内で スコープを限定 できても、関数全体で再利用される設計。
  • errの再利用 がバグの温床になり、後のコードで誤って参照されるリスク。
  • スコープ管理 がGoの構文上困難で、意図しないバグ誘発。

nilの二重性と混乱

  • Goのnil には 2種類 あり、ポインタとインターフェースで挙動が異なる。
  • 同じnil表示 でも、比較結果が一貫しない例が存在。
  • 「What color is your nil?」 と揶揄される設計ミス。
  • NULLの二重性 はバグや混乱の原因。

移植性の低さ

  • 条件付きコンパイル のためにファイル冒頭にコメントを書く仕様。
  • 移植性重視 の観点から見て、保守性を著しく損なう設計。
  • 過去の経験 を無視した愚策であり、現場での運用に不向き。

appendの所有権不明問題

  • append関数 の挙動が直感的でなく、 スライスの所有権 が不明確。
  • 参照のままか、コピーが発生するか 予測困難
  • 意図しない結果 を招きやすく、言語仕様の不親切さを露呈。

deferによるリソース管理の曖昧さ

  • deferによる解放 はRAIIのような自動リソース管理に劣る。
  • JavaやPython のような構文的リソース管理がない。
  • どのリソースでdeferが必要か 明示されておらず、手動管理の煩雑さ。
  • 二重Close や例外時の安全性も保証されず、標準ライブラリでも例外を飲み込む設計。

例外(panic)と例外安全性の問題

  • Goは例外(panic)を推奨しない が、例外安全なコードを書く必要がある。
  • mutexのunlock など、panic発生時も安全に動作させる必要性。
  • 標準ライブラリ でも例外(panic)を飲み込む場面があり、全ての開発者が例外安全を意識せざるを得ない。
  • 例外のデメリットだけ が強調され、メリットは享受できない設計。

UTF-8前提の文字列仕様とデータ損失

  • Goのstring型 はUTF-8前提で、 バイナリデータ を扱うとデータ損失の危険。
  • UTF-8外のファイル名 やデータが黙ってスキップされる事例。
  • 過去資産の互換性 を無視した設計で、データ消失のリスク。

メモリ使用量制御の難しさ

  • GC(ガベージコレクション) の挙動が制御しづらく、 メモリ肥大化 が発生。
  • クラウドやコンテナ 環境でのコスト増加や効率低下。
  • runtime.GC() による手動制御は非推奨とされ、現場での対応が困難。
  • 他言語への書き直し を余儀なくされるケース。

結論:既知の問題を無視した設計

  • Goの設計上の問題 は、既に解決策が知られていたにも関わらず放置。
  • COBOLやJavaの設計論争 とは異なり、歴史的知見を活かせなかった例。
  • Goコードベース の維持管理が今後も大きな課題。

他者による参考記事

Hackerたちの意見

Goを使い始めてから、1.0以前のフルタイムの仕事でもほぼ毎回使ってるよ。チームの人たちが基本を覚えるのも簡単だし、全体的にスムーズに動いてる(最新のGoにアップデートするのもあんまり心配しないし)。便利な機能がほとんど内蔵されてるし、コンパイルも早い。並行処理はちょっと難しいけど、時間をかければデータフローをGoで表現するのが楽しい。型システムもほとんどの場合便利だけど、時々ちょっと冗長かな。全体的に信頼できるツールだよ。でも、この記事のいくつかのポイントには同意せざるを得ないな。Goは古い考え方の人たちによって設計されたから、実用的な便利さを見失ってる部分がある気がする。ただ、これはあくまで私の感覚で、もしこれらの quirks を解決してたらGoはもっと悪くなってたかもしれない。公平に言うと、ここ数年でquirksを直す余裕が増えてきたと思う。例えば、ジェネリクスやカスタムイテレータが見られるとは思わなかったし。RAMやポータビリティに関するポイントは、ほとんどが個人的な不満に感じるけど、もちろんもっと良ければ嬉しいよね。でも、GoのGCはほとんどのプログラムで問題を引き起こすことはないし、大規模でもデバッグはそんなに難しくないよ。それに、Goはほとんどのプラットフォームで動くから、ソフトウェアを出荷するのに困ることはない。ただ、エラーやnilの問題はまだ気になるな。Result[Ok, Err]やOptional[T]があればいいのにって思うことがよくある。

タイプシステムはほとんどの時間、とても便利だね。どの宇宙の話?

Goは、原則にこだわりすぎた古い世代の人たちによって設計されたんだと思う。実用的な便利さを見失ってる。逆に言うと、目の前の問題を素早く解決するための実用的な便利さにこだわったんじゃないかな。最初の原則から問題を分析して、正しく解決するのではなく(自分たちが発明したものを使わないで)。GoのファイルシステムAPIがその完璧な例だよ。ファイルを開く必要がある?いいね、func Open(name string) (*File, error)って関数を作るから、今すぐファイルを開けるよ、終わり。ファイル名が有効なUTF-8じゃなかったらどうするの?気にしないよ、Goを使ってた最初の5年間はそんなことなかったし。

全体的に信頼できるツールだね。 同意するよ。Goの標準ライブラリは素晴らしい。Pythonとは違って、Goには依存関係地獄もないし。オーブン-readyのバイナリを出荷するだけでいい。じゃあ代わりは?Java?分岐したフォークを使う必要があるライセンスの問題。さらに、Goはサーバーサイドのデプロイには特に扱いやすいかも。Zig?Rust?複雑な学習曲線があるし、例えばRustのクレートを選ぶのは依存関係地獄を再導入して、サプライチェーン攻撃の可能性もある。

Discordがスタックの一部をRustで書き直したのは、GoのGCが問題を引き起こしてたからってよく言われるよね。コードは、毎秒数十億(B単位)のメッセージを処理する中央ルーティングサーバーのホットパスにあったとか。君がDiscordを作るわけじゃないし、GCは君のメトリクスにほとんど影響しないと思うよ。GCは全然問題ない。

開発者の使いやすさに関しては、「非常に意見が強く、非常に標準的で、やり方が一つ」って部分をしっかり抑えてると思う。大規模なマイクロサービスアーキテクチャで作業するのが楽しいし、各リポジトリでスタイルが違ったり、フォーマットの議論を避けたりする必要がないのがいいよね。ただ、どの「Goのやり方」を選ぶかの選択がちょっと古くなってる気がする。人々は、オフバイワンのリスクがあるループよりも、map/filterメソッドを期待してるし、TypeScriptの賢さを持った型システム(機能は少ないけど、より厳格に適用される)を求めてる。エラーハンドリングも面倒だしね。そういう機能を実装するのは難しいのはわかるけど、若い開発者にとっては、JavaScriptが母国語でCじゃないから、Goは時々売り込みが難しい理由だと思う。

最近、新しい仕事のためにGoを書き始めたんだけど、20年間コンパイル言語に触れてなかったから、ちょっと辛い。趣味でDevKitArmの開発はやってたけどね。好みの問題だとは思うけど、ほんとに嫌な感じ。デフォルトのパラメータ値がないし、エラーハンドリングも臭いし、プロダクションではまともなスタックトレースもない。オブジェクト指向の構文も、各関数に変な参照を追加するだけだし。ポインタも…C/C++時代を思い出させる。1999年に大学にいた頃の25年前の技術でプログラミングしてるみたい。

Goは、古い学校の人たちによってデザインされたもので、原則に固執しすぎて実用的な便利さを見失っているように感じる。彼らが固執している二つの原則は「コンパイラを書くのを楽にすること」と「コンパイルを速くすること」って感じ。良い目標だけど、開発者向けにはほとんど配慮されてない。

「同時実行性は難しい」ってのは、ほとんどの言語に当てはまることだよね。たとえ簡単に同時実行がサポートされている言語でも、正しく使うのが難しい。ポータビリティには特に問題ないけど、Goが光るのはAWS Lambdaみたいな、速い実行が求められる場面だと思う。ユーザーのシステムにコードを配布するわけじゃないし。

「Optional[T]が欲しいと思うことがよくある。」 まあ、広いエコシステムとの互換性を気にしないなら、自分でOptionalを作るのも全然アリだよ: type Optional[Value any] struct { value Value exists bool } // 新しい空のOptional。 func NewValue any Optional[Value] {} // 値の新しいOptional。 func Of[Value any](value Value) Optional[Value] {} // ポインタの新しいOptional。 func OfPointer[Value any](value *Value) Optional[Value] {} // 値を取得する唯一の一般的な方法。 func (o Optional[Value]) Get() (Value, bool) {} // 値を取得するか、パニック。 func (o Optional[Value]) MustGet() Value {} // 値を取得するか、デフォルト。 func (o Optional[Value]) GetOrElse(defaultValue Value) Value {} // JSONサポート。 func (o Optional[Value]) MarshalJSON() ([]byte, error) {} func (o *Optional[Value]) UnmarshalJSON(data []byte) error {} // DBサポート。 func (o *Optional[Value]) Scan(value any) error {} func (o Optional[Value]) Value() (driver.Value, error) {} でも、他の人たちとの互換性を気にするだろうから…うん、Goのオプショナリティの扱い方がポインタを使うのは本当に面倒だよね。

並行性は難しいね。Go言語とそのランタイムは、マルチコアCPUでの並行性をシームレスに扱える唯一のシステムだと思う。CSPのような(ゴルーチン/チャネル)形式を使っていて、理解しやすいんだ。PythonはGILや非同期ライブラリがあって、扱いがめちゃくちゃだし。C、C++、Javaなどはスレッドを実装するために外部ライブラリが必要で、それは言語の文脈では理解しづらい。だから、GoはHTTPサーバー(またはサービス)のユースケースにぴったりで、私の経験では他に比べるものがないよ。

なんでdeferが関数スコープで機能するのか、レキシカルスコープじゃないのか、未だに理解できないし、誰もその理由を説明してくれない。実際、これが驚きだったのは、ファイルをループで処理するコードを書いたときに、ファイルのリストが大きくなったらクラッシュし始めたから。deferが関数が返るまでハンドルを閉じなかったんだ。別のGoプログラマーに聞いたら、ループの本体を匿名関数でラップして呼び出すように言われた。それ以外は(他の小さな不満もあるけど)、Goは楽しくてコンパクトな言語だと思う。効率的な構文で、あまり可愛くしようとする人を促さない感じ。Goの旅は、かなり大きなC#プロジェクトを書き直すところから始まったけど、C#の機能の10%しかないのに、コードが小さくなったのには驚いた。GCの割り当てを毎回強制しないようなパフォーマンス重視のデフォルトを促進してるし、シリアライズみたいなコード生成のための良い内蔵サポートもある。C#がORMみたいなもので「SQLの代わりにC#を書ける」って主張するのとは違って、GoではSQLを書くときはSQLを書くし、gRPCはprotobufの仕様を書くことで実現する。

Hacker Newsで議論の続きを見る