なぜGo言語 (golang) はよい言語なのか・Goでプログラムを書くべき理由
結論としてはGo言語には以下のようないくつかの長所があり、現実路線で非常にバランスがとれた言語だと思います。 これらの長所のために失われたメリットも当然いくつもありますが、一定程度以上の規模のプロジェクトで利用する言語の選択肢としては現存するプログラミング言語の中では一番か二番目によいのではないかと思います。
- コンパイルが速い (vs. C++)
- GCとメモリ安全性 (vs. C++)
- 妥当で現実的なレベルの型安全性 (vs. Python/Ruby)
-
実行時パフォーマンスが良さ (vs. Python/Ruby)
- 現実問題、ある程度の規模と期間のプロジェクトになると型検証があるとリファクタリングなどがだいぶ楽になるのでありがたい。
- 型があるので自然と実行時パフォーマンスも良い
-
標準ライブラリが整備されている (vs. C++)
- むしろ標準ライブラリにjsonのparserすら存在しないC++がおかしい...
-
シンプルでバランスの取れた言語仕様。習得が比較的容易 (vs. C++, Haskell, その他多くの言語)
- すでに1つ手続き型言語をそれなりに使えている人ならば、軽く仕様を読めば何となく使えてしまえると思います。
- ただ他のメジャーな手続き型言語との違いから来るはまりどころが幾つかあるのできちんと理解するのはそれなりに労力は必要。
- とはいえC++とかHaskellよりは確実に簡単だと思う。
-
非知的なプログラマのためにデザインされている。
- 言語デザインとして正しく使うのが難しい、乱用するとプログラムを無意味に複雑にしてしまう機能が排除されている。
- 高度な型システム・継承・Generics・例外・イベントモデルによる並行処理など。
- 面白みに欠くプログラミング言語である。そのようにデザインされている。
-
スレッドモデルによる並行プログラミング (vs. Node.js)
- 一時期Node.jsなどでイベントモデルによるプログラミングが何故かもてはやされていましたが、人類にはイベントモデルは無意味に難しすぎます。
- 百害あって一利ぐらいしかない。
- ようやくJavaScriptですらasync awaitによるスレッドモデルへの回帰がみられる。
- 言語標準の方法としてgoroutineを使ったスレッドモデルによる並行処理が強く推奨されているのは素晴らしいデザインだと思います。
- イベントモデル vs スレッドモデルの不毛な論争をさけることができる。
- Dockerなどの素晴らしい大規模プロジェクトでの実績がある。
-
実行環境としてJVMやインタープリタを必要としないのでデプロイが軽量で済むし、比較的容易である。
- 純粋にGoで書かれた(
cgo
を使っていない)プログラムであればCGO_ENABLED=0
と一緒にコンパイルすると生成されるプログラムは静的リンクされたものとなる。 - 単一ファイルをデプロイするだけでよいのでデプロイが容易。
- とはいえ仮想マシンやコンテナで環境をまるごとデプロイする既存の技術が発達している現在では容易さによるメリットは限定的。
- 実行に必要となるものが少ないので仮想マシンやコンテナのサイズは小さくて済むのでデプロイはその分軽量になる。
- 例えばDockerのAlpineイメージと組み合わせて20~30MB程度の軽量なDockerコンテナでデプロイができます。
- 純粋にGoで書かれた(
目次
- シンプルでバランスの取れた言語仕様
- なぜGoは"悪い"言語なのか
- Generics (template)がない
- 継承がない
- Errors as values (例外が推奨されない)
- 非知的なプログラマのためにデザインされている (Designed for non-intelligent programmers)
- golang 関連記事
-
- すべて読むのにそれほど多くの時間は必要ない。
- C++やHaskellに比べれば遥かに習得が容易である。 すでに手続き型言語を1つでも使いこなせるプログラマならば軽く仕様を眺めればすぐに何となく使えるようになるであろう。 ただしそれでもはまりどころはいくつもあるので
- いくつかの"クール"な言語機能がGo言語 (golang) ではサポートされていない。代表的なものは 「例外」、「Generics (template)」、「継承」
- ただ他のメジャーな手続き型言語との違いから来るはまりどころが幾つかあるのできちんと理解するのはそれなりに労力は必要ではある点は注意。
Goに対する批判は数多く存在します。それのどれにもきちんとした理由はあると思います。主な批判は大きく
- Generics (template) がない
- 継承がない
- 例外がない。まるで1970年代に設計されたかのようである。
- 非知的なプログラマのためにデザインされている。
に集約されるように思います。
Go言語(golang)にはGenericsはありません。 JavaのGeneric TypesとかC++のテンプレートで書けるようなことはGoでは書けません。 ただ配列(スライス), mapについては特別に言語でサポートされているのでJavaやC++で総称型を使うケースの9割ぐらいはカバーされるとは思います。Java1.5より前のJavaのように連想配列や可変長配列を使うのに常にキャストが必要になったりはしません。そのためGenericsがサポートされていないことはそれほど大きな問題にはなりません。
上述のFAQにかかれている通り、GenericsをサポートしないことによってGo(golang)の型システムはシンプルに保たれています。 C++のテンプレートの意味不明なコンパイルエラーに悩まされることはGo言語(golang)ではありません。 またGenericsも継承と同じように本来使われるべきでない場所で乱用される言語機能の1つだと思います。Go/Javaのinterfaceに相当するもので十分なものに無意味にGenerics (template)が使われていて保守性・可読性の低いコードは特にC++でよく見かけます。そのためカスタムのコンテナを定義するのにGenericsがないのは不便だけれど、Genericsが存在することで生じる複雑性や乱用による不利益を避ける方を選んだのは悪いことではなかったと思います。
一方で、mapと配列以外のカスタムのコンテナが使いたい時にはGenericsがないのは不便なのも確かなので、将来うまい落とし所がみつかることを期待したいです(Generics may well be added at some point.)。
現時点でどうしてもGenericsのようなことがやりたければ、go generate
でコードを自動生成するのも1つの手ではあります。あまり多用しすぎない方がよいのかなとは思いますが。
Goには継承はありません。 そもそも継承はプログラミング言語にあまり必要ない機能だと思います。 継承が本当に有益なこともありますが、経験上大半のケースでは設計を手抜きするために継承が使われていて、結果長い目で見た際のreadabilityやmaintainabilityが著しく劣化してしまっていることが多いと思います。Composition over inheritanceやリスコフの置換原則のような基本的な原則が守られておらず(そもそも多くの人は名前すら知らない)、単に一部のコードをクラス間で共有するために継承が使われていて可読性が著しく低いコードもよく目にします。
そのため、そもそもプログラミング言語が継承をサポートしないというのは非常に良いデザインだなと思います。継承が非常に有益な場合も稀にあるもの分かりますが、大規模プロジェクトにおいては正しく使われない害のほうが確実に大きいです。
Go言語のFAQにあるように、Goには例外がありません。panic, recoverで例外と同じようなことはできますが、Javaの例外のように気軽に使ってはなりません。個人的にはこのFAQにかかれていることには概ね同意します。
例外で返されたエラーを try {...} catch (Exception e) {...}
みたいに処理しないといけないのは無意味に複雑なように思います。
それだけならよいですが、例外が発生するとコードが想定外の順序で実行されて困ったり、何故かこのコードが実行されないなと思ったら、その前に例外で大域脱出していて、しかもその例外が予想外のところでcatchされ握りつぶされていたり、と例外を大規模なプロジェクトの中で正しく扱うのは中々に困難だと思います。
Goの例外は極力使わず、エラーを値として扱うポリシーはよいもの(特に大規模なプロジェクトで、エラーハンドリングが大切なプロジェクトでは)だと思います。
ただ一方で、ちょっとした使い捨ての便利ツールを書く場合や、とりあえずプロトタイプで正常系だけ書きたい時、 あるいは異常が発生したらプログラムを停止してしまって良いような起動時の初期化処理を書く時には、正直Goのエラーハンドリングはかなり面倒くさいです。 こういうタイプのコードでは外部ライブラリの呼び出しやファイル、データベースなどの外部リソースへのアクセスが大きな割合を占めます。そして、そうした処理はほとんどの場合 error が発生しうるのでそれぞれの処理に対してエラーハンドリングを行う必要があります。 場合によってはコードのかなりの割合の行が
if err != nil {
return nil, err
}
の繰り返しで占められてしまうこともあるでしょう。これに対しては根本的な解決策はないように思います。エラーが発生した場合はエラーメッセージを出力して処理を中断してしまって問題ない
- 使い捨てあるいは内部ツールで開発者・利用者の数が数人でエラーハンドリングがあまり重要ではない時
- 正常系だけとりあえずプロトタイプしたい時
には例外の方が便利であり、正直Go言語ではあまり効率的にコードが書けないような気がします。個人的にはそういう用途にはPythonなどを使うのが正しい解決策のように思えます。何でもGoで書く必要はないのですから。 型の存在すら簡単なプロトタイピングをするのには少し鬱陶しいですしどんな言語も適材適所だと思います。
Goは知的でないプログラマのためにデザインされていると批判されることがあります。そして「知的でないプログラマ」のためにデザインされているというのは指摘自体は正しいと思います。実際Rob PikeもFrom Parallel to Concurrentの20:40~で次のように述べています
The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
要するにGoogle社員のような、研究者ではなく大学を出たばかりでC++, Java, Pythonの経験しかない「素晴らしいプログラミング言語」を理解する能力が欠けているエンジニアでも簡単に理解し、大きなシステムを構築するのに使えるようにGo言語はデザインされているのです。
Goは言語デザインとして正しく使うのが難しい、乱用するとプログラムを無意味に複雑にしてしまう機能が排除されています(高度な型システム・継承・Generics・例外・イベントモデルによる並行処理など)。 そのため、ある意味ではGoは「知的な」プログラマにとっては面白みに欠く言語だと思います。実際、Goを勉強しても概念的には目新しいものはなくて勉強することそのもので何かが学べるとは僕は全く思いません。 一方でプログラミング言語を習得することのゴールは、それを使って何か大規模なソフトウェアをチームで作ることであるのだと思います。 チームで大規模なソフトウェアを開発することはそれ自体が非常に難しいことです。そのゴールのためにプログラミング言語自体はシンプルに保ち、それ以外の部分により多くの労力をさけるようにするというのは僕は正しい判断だと思います。
-
- 僕がGoを使い始めたときにはまったところをまとめてあります。 C++, C#, Java, Pythonなどの経験者がGoを始めるときに、気をつけておくとよいなと思った点がまとめてあります。 僕と同じ間違えをして時間を無駄にする人が少しでも減るとうれしい。
-
easycsv - Go言語 (golang) でcsvを手軽に扱うためのライブラリ
reflect
の勉強を兼ねて作った標準のencoding/csv
より高機能なcsvライブラリ。
-
Go言語(Golang) ファイル・I/O関係のよく使う基本ライブラリ
- Go言語でプログラムを書く際に欠かせないファイルなどの入出力(IO)関係の基本的なライブラリの一覧。この辺のライブラリはかなりよく使うのでパッケージ名や型名を覚えておくといちいちドキュメントを検索せずにすんで良いかも。