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

「Go」はまだ良くない

概要

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の仕様を書くことで実現する。

深い理由はないと思うけど、そんなに重要かな?

両方の言語で作業したことがあるけど、ブロックレベルの言語を使うときに、条件内で関数レベルのdeferが使えたらいいなって思うことがある。

たまにレキシカルスコープが欲しいときもあれば、関数スコープが必要なときもあるよね。例えば、ループでたくさんのファイルを開いて、その関数の残りの部分で全部開いたままにしておきたいとき。今は関数スコープだけど、レキシカルスコープが必要なら、関数にラップすればいい。もしレキシカルスコープだったら、関数スコープが必要なときはどうするの?

レキシカルスコープには、deferを置くためのスタックがないんだよね。

  1. 関数にラップするまでインデントが一段階少なくて済む 2. メカニズムはコールスタックやスタックのアンワインドに結びついてる 3. goto failからCに来たときは自然に感じる(ループ内でdeferを使いたいときにイライラするけど、そのループの本体が関数にならなきゃいけないのがね)

C#でSQLを書くことも、protobuf仕様を使うこともできるよ。もちろん他の選択肢もあるけどね。

クライアントのために誰かが書いたGoの静的サイトジェネレーターを拡張する仕事を少ししたことがある。コードはとても明確で読みやすかったけど、言語の多くの粗い部分のせいで拡張が難しかった。簡単な変更でも、すぐには分からない方法で多くのコードを変更する必要があった。「シンプルさ」の名のもとにカプセル化や抽象化の能力が妨げられている。抽象化は、シンプルで拡張しやすいコードを実現するための主要な方法だ。ジョン・アウスターハウスは、複雑なプログラムを大きいとか理解しにくいというよりも、拡張が難しいものと定義した。平均的なGoプログラムはこの原則をよく破っている気がする。プログラムは「シンプル」に見えるけど、拡張が難しくて面倒。Goは「裸の王様」の例だ。人々に「理解できていないだけ」とか「違ったやり方なんだ」と言っても、納得できない。唯一の良い点はシンプルな開発体験だね。

Goについての人々の話し方がすごく変だと思う。批判が出ると、ほとんどの人が「言語はただの『普通』だ」と反応して、欲しいと思うことに対してちょっと恥じらいを感じさせる。Goはシンプルだと言われるけど、マップのキーのリストを取得するためにforループを書くのはシンプルじゃないよね。

個人的にはGoが好きじゃないし、いくつかの欠点もあるけど、それでも人気がある理由はあるよ。Goは、信頼性が高くて、重いマルチスレッドに頼らない高い並行性のサービスを書くのがかなり簡単な言語だから。これもgoroutineモデルのおかげ。Googleが出したときには、他にあまり人気のある静的コンパイル言語がなかったし、今でもほとんどない。似たようなスペースにいる本当の競争相手はJavaの新しい仮想スレッドくらいだね。async/awaitを持つ言語は似たようなことを約束するけど、実際には多くの複雑さ(非同期タスクでのブロッキングを避けること、関数の色付けなど)に悩まされてる。Erlangは全然違うタイプの言語だから、ここではカウントしないけど。だから、Goは多くの欠点があるにもかかわらず、goroutineとGoogleプロジェクトの信頼性のおかげで人気があると言えるね。

あなたの意見では、どの現代言語が新しいプロジェクトに適していると思う?

async/awaitには本当に痛いポイントがあるけど、批判は大げさなことが多いと思う。純粋なasyncにすればほとんどの問題は解決するけど、古い同期コードとasyncを混ぜるのはかなり難しい。私の経験は主にC#だけど、そこでのasync/awaitはすごくうまく機能してる。問題を避けるためには基本を知っておく必要があるけど、これは基本的にすべての並行性に当てはまることだよ。どれも足元をすくわれる危険がある。

そして今でもほとんど競争相手がいない - 同じような領域にいる唯一の本当の競争相手は、新しい仮想スレッドを持つJavaだね。Googleは今でもGoよりもJavaをよく使ってる。

ゆっくりだけど確実に、JVMはGoとの差を縮めてきてる。仮想スレッドやZGC、リリパット、レイデン、バルハラなどの取り組みで、JVMはギャップを埋めてきた。Java 8から25への変化はまるで別世界だよ。未来は明るいね。Javaは徐々に作業がかなり快適になる言語機能を取り入れてきてる。

Rustは数えられるね。今のところ、マイクロサービスの分野でかなり人気が出てきてるみたい。なんでか想像するのは簡単だよね。マルチスレッドが楽だし、メモリの使い方も少ない。レイテンシも素晴らしい。

Goの比較的厳格でシンプルなところも、LLM支援プログラミングにはいい選択肢だと思う。インターネットから集めたGo 1.xのコードは、モデルに組み込まれてもまだ完全に有効で、最新バージョンでコンパイルできるからね。

技術的には、「10億ドルのミス」という言葉は1965年に作られたけど、2025年には「100億ドルのミス」になるだろうね。もしコストを住宅の観点で測るなら、「210億ドルのミス」になるかも。 :^/

今いるところでは、みんなにTypescriptのコードを書かせるのがまるでGolangみたいで、他にもめちゃくちゃな決定が多いよ。ユニットテストはサービスの境界だけ、ロジックを純粋な関数に分けるな、UIテストは書くな、みたいな。入る前にその組織のコードを見せてもらうことを忘れないようにしないと。 (誰が雇ってるかは関係ないけど、プロフィールにメールアドレス載せてるから)

Javaの人たちがPythonを書くの見たことある?同じ感じ :)

ああ、そうそう。専門家を雇って、その専門家がやるべき仕事をどうやるべきか教える場所で働くのが大好きだよ。

これやってるけど、すごくうまくいってると思う… myfunc(arg: string): Value | Err もうTypeScriptでthrowはしないようにしてる。Goみたいにエラーチェックしてるんだ。Goのバックエンドと一緒に使うと、コンテキストスイッチがすごく楽になるよ…

Goのクロスコンパイルは簡単だよ。静的バイナリはどこでも動くし、暗号ライブラリはletsencryptみたいな各種CAの基盤になってて素晴らしい。グリーンスレッドも面白いね。低コストで1000個以上作れるから、いろんなデザインが可能になる。deferについての不満はちょっと些細なことだと思う。実際の大きな問題はインポートの仕組みかな。GitHubについて知っているのはいいけど、依存関係を他のもの、特にローカルのものに置き換えるのが難しいのが問題。ファイルの強制レイアウトやcmdディレクトリとかもね。全部我慢できるけど、モジュールは一番時間を無駄にした部分で、苦労した。

GitHubについて知っているのはいいけど、依存関係を他のもの、特にローカルのものに置き換えるのが難しい。go.modreplaceを使うか、ローカルでハッキングしてるならgo.workを使えばいいんじゃない?

5年以上、大きなGolangプロジェクトでほぼ専念してきたけど、これには共感する。プロジェクトの一部はできるだけメモリを使わないように求められていて、そのせいでGoの粗い部分に何度もぶつかってきた。ガーベジコレクタがすぐに片付けてくれない問題や、ヒープの断片化の問題があって(Goはコンパクティングガーベジコレクタを持たないという無限の知恵で)、完全にアロケーションを避けなきゃいけないこともあった。ああ、そういう問題があると、デバッグがすごく難しい。ヒーププロファイルを取れるけど、それはヒープ内の生きているオブジェクトについてしか教えてくれない。ゴミや断片化については教えてくれないから、問題の診断は運任せになる。例えば、ヒーププロファイルでは関数Xが1KBのメモリしか割り当ててないけど、ホットループで呼ばれてるから、実際には20MBのゴミが生成されてる可能性がある。静的バッファをたくさん事前に割り当てて再利用してるけど、それが所有権の問題を引き起こすこともある。記事に出てたappendの罠みたいなやつね。標準ライブラリの一部を再実装しなきゃいけなかったこともある。非標準のユースケースがあるのは理解してるし、ほとんどのプログラマーはそこまでメモリ使用に神経質じゃないと思う。でも、私たちはそうなんだ。言語と戦ってる気分にならないといいな。

こういう時は、オフヒープに移す方が楽だって気づいたけど、GC言語では完全に簡単ではないし、粗い部分もたくさん生まれる。Goで実質的にC++やRustを書いてるなら、その部分はそれぞれの言語で書き直した方がいいかもね :)

アリーナ実験に興味があるんじゃないかな。ただ、今は一時停止中みたいだけど。

新しい「グリーンティー」GCが助けになるかも?「メモリ中心ではないが、少なくともメモリに気を配った並列マークアルゴリズム」と説明されてるよ。 https://github.com/golang/go/issues/73581

1.0が出る前から今まで、Goが大好きだよ。細かいことを言えば色々あるけど、「まだ良くない」っていうのは変な意見だと思う。クリエイターたちがプロジェクトから離れていくことで、中心的なビジョンを維持するのは難しいだろうし、それが言語を悪くすると思う(トレードオフも無意味になるし)。「サーバーを書くための言語」として固定されるのは、重要な頭の中のシェアを失う原因になるし、それがRustに流れたり、Pythonに残ったりするんじゃないかな。もしかしたら、Visual Basicがどれだけひどかったかを語るのが楽しいだけかもね。確かにそれは事実だけど、必要な人たちはそれをうまく使ってたし。

Goには欠点もあるけど、他のサーバーサイド言語が提供できない絶妙なポイントを突いてると思う。NodeやPythonより速いし、どちらよりも良い型システムを持ってる。Rustよりも学習曲線がずっと楽だし、良い標準ライブラリとツールも揃ってる。シンプルな構文で、だいたい一つのやり方しかないし。エラーハンドリングには問題があるけど、Nodeよりはまだマシだと思う。Nodeではcatch句が「エラー」として何でも受け取ることがあるからね。他に同じことをする言語を見逃してるのかな?俺はGoのファンじゃないし、キャリアの中で主にNodeでバックエンドを書いてきたけど、最近Goを探求してるんだ。

もしかしたらNimかも。でも、あんまり普及してないし、エコシステムも相対的に未成熟だね。

NodeやPythonより速くて、どちらよりも優れた型システムを持ってる。Rustよりも学習曲線がずっと楽だし、良い標準ライブラリとツールも揃ってる。シンプルな構文で、だいたい一つのやり方しかないし。エラーハンドリングには問題があるけど、Nodeよりはマシだと思う。Nodeではcatch句が「エラー」として何でも受け取る可能性があるからね。この段落をJavaやC#についても書けそうな気がする。

そうそう、大きな問題はほとんどの言語が多少の粗さを抱えてるってこと。Goはパフォーマンスが良くてポータブルで、良いランタイムとエコシステムを持ってる。でも、nilポインタやゼロ値、デストラクタなし、マクロもないっていう欠点もある。(マクロが悪いって言う人もいるけど、コード生成の方がもっと厄介で、Goはマクロがない分、たくさんのコード生成を使わざるを得ないんだよね。)欠点が少ない言語もあるけど、たいていはもっと複雑(例えばRust)で、Goの問題の多くは、シンプルさを追求しすぎた創始者たちのせいなんだ。