言語設計FAQ

生立ち

このプロジェクトにはどのような歴史がありますか?

Robert Griesemer、Rob PikeおよびKen THompsonは2007年9月21日にホワイトボードに新しい言語の目標をスケッチすることを始めました。数日かけて、数ある目標から、何をするのかという計画と、この言語の美しい理想像が定まりました。他の仕事と並行して、隙間時間を利用して設計が続けられました。2008年1月までに、Kenはコンパイラについてのアイディアを追求することを始めました。それは、コンパイラの出力としてCのコードを生成することでした。この年の中頃までに、この言語はフルタイムのプロジェクトとなり、製品版コンパイラを製作するために十分な時間を費やすことになりました。2008年5月には、Ian Taylorが自主的に、ドラフト版の仕様をもとにして、GCC用のフロントエンド作りに取りかかりました。Russ Coxは2008年の終わり頃に参加し、言語とライブラリをプロトタイプから現実的な実装に導くことを助けました。

ほかにも沢山の人々にアイディアを出していただき、議論をし、ソースコードに寄与していただきました。

なぜ新しい言語を作るのですか?

Goは、既存のシステムプログラミング用言語と環境への欲求不満から生まれた言語です。プログラミングはとても難しいものとなってしまいましたが、それは言語の選択が一因でもあります。効率的なコンパイル、効率的な実行、または容易なプログラミングのいずれかを選ばなければなりません。なぜなら、主要言語において、これら3つを満たす言語は無かったからです。プログラマは、安全性と効率を楽に得ることを選択するために、C++ではなく、PythonやJavaScriptのような動的型付け言語や、C++より自由度を下げて拡張したJavaに乗り換えることが可能ではありました。

Goは、インタプリタ型の動的型付け言語の容易なプログラミングと、コンパイラ型の静的型付け言語の効率と安全性を兼ね備える試みです。また、現代的であり、ネットワークコンピューティングおよびマルチコアコンピューティングをサポートする狙いがあります。最後に、高速な実行を狙いとしています。その上で、1つのコンピュータで大きな実行可能ファイルをビルドすることが数秒で完了するのは当然のことです。これらの目標を実現するためには、数々の言語上の課題に取り組まなくてはなりません。表現力が豊かで軽量なシステム、同時並行性とガーベジコレクション、堅牢な依存関係の仕様、などなどを実現する必要があります。これらはライブラリやツールで上手に解決することは不可能です。ゆえに、新しい言語が生まれたのです。

Goの祖先は何ですか?

Goは基本的な文法の大部分はC言語系から受け継いでいます。また、宣言やパッケージについては、Pascal、Modula、Oberonから大きな影響を受けています。加えて、並行処理については、Tony HoareのCSP (Communicating Sequential Process) に影響を受けている言語であるNewsqueak、Limboから同じアイディアを取り入れています。しかしながら、全体としては新しい言語です。あらゆる点において、Goはプログラマが何をし、どうプログラムを作るのかということを考えられて設計されています。少なくとも私たち自身が行うプログラミングがより効率的に、より楽しくあるようにです。

What are the guiding principles in the design?

Programming today involves too much bookkeeping, repetition, and clerical work. As Dick Gabriel says, “Old programs read like quiet conversations between a well-spoken research worker and a well-studied mechanical colleague, not as a debate with a compiler. Who’d have guessed sophistication bought such noise?” The sophistication is worthwhile—no one wants to go back to the old languages—but can it be more quietly achieved?

Go attempts to reduce the amount of typing in both senses of the word. Throughout its design, we have tried to reduce clutter and complexity. There are no forward declarations and no header files; everything is declared exactly once. Initialization is expressive, automatic, and easy to use. Syntax is clean and light on keywords. Stuttering (foo.Foo* myFoo = new(foo.Foo)) is reduced by simple type derivation using the := declare-and-initialize construct. And perhaps most radically, there is no type hierarchy: types just are, they don’t have to announce their relationships. These simplifications allow Go to be expressive yet comprehensible without sacrificing, well, sophistication.

Another important principle is to keep the concepts orthogonal. Methods can be implemented for any type; structures represent data while interfaces represent abstraction; and so on. Orthogonality makes it easier to understand what happens when things combine.

C言語からの変化点

なぜC言語とこんなに文法が違うんですか?

宣言文を除くと、違いは大きくはありません。2つの希望によってこのようになっています。まず最初に、文法は軽く感じるようにすべきで、義務的なキーワードがあまりにも多かったり、繰り返し書かなければならなかったり、呪文のようだったりというのは辞めたいと考えていました。二つ目は、プログラミング言語はシンボルテーブルをパースしなくても簡単に分析できるように設計されました。この設計方針の影で、デバッガー、依存関係のアナライザ、自動ドキュメント抽出ツール、IDEプラグインなどのツールの開発がより簡単になります。C言語とその子孫の言語はこの点が困難で悪名をとどろかせていました。

なぜ型宣言が後方になったのですか?

もしもC言語を使用してきたのであれば、後方だけになります。C言語では、変数は式のように宣言され、型が指示されます。これは良い考えで、型と式の文法がごちゃごちゃに混ざることはありませんが、実行結果は混乱することがあります。関数ポインタの例を思い出してください。Goでは式と型の文法はほぼ切り離されていて、シンプルになっています。ポインタのための*プリフィックスはこのルールの例外として拡張しています。C言語では以下の宣言があったとすると:

int* a, b;

この宣言ではaはポインタになりますが、bはなりません。Goの場合は

var a, b *int;

このどちらもポインタになります。こちらの方が明快で、より整然としています。また、 := の短い宣言の形態についても、完全な変数宣言と同じ順序であるべきです。そのため

var a uint64 = 1;

これと、以下の宣言は同じ意味になります。

a := uint64(1);

型のための文法と、式の文法がまったく異なっているため、パースをするのが簡単になります。 funcchan といったキーワードも、文法を分かりやすく維持するのに役立っています。

なぜポインタ演算がないんですか?

安全のためのです。ポインタ演算がないおかげで、不正なアドレスを算出するようなことができないような言語を作ることができました。コンパイラとハードウェアの技術が進歩したおかげでポインタ演算を使ってループで実現するのと同じぐらい効率的に、配列のインデックスを使ったループでアドレスをポイントすることができるようになりました。また、ポインタ演算がないおかげで、ガーベジコレクタの実装もシンプルになりました。

なぜ ++ と - - と not が式なんですか?なぜ後置なんですか?前置ではないのですか?

ポインタ演算がないので、使い勝手の良い、値の前置、後置のインクリメント演算子はなくなりました。式の階層構造からこれらが取り除かれたことによって、式の文法はシンプルになり、++と–の評価順に関する複雑な問題(f(i++)p[i] = q[++i] の式がいつiを増やすのかという問題)をうまく除去することができます。シンプルというのは重要なことです。後置か前置かという点に関しては、どちらもうまく動作しますが、後置バージョンの方が伝統的に使われてきました。名前に後置のインクリメントを含む言語では、そのライブラリのSTLでは、皮肉なことに、前置を強制させていました。

なぜガーベジコレクタを動作させるのですか?実行コストが高いのではないですか?

システムプログラミングのおいて、定型的な作業の中でもっともソースコードを占めているものの一つがメモリ管理です。私たちは、プログラマーのオーバーヘッドを取り除くことが重要だと考えています。ここ数年のガーベジコレクタの技術の進歩により、システムプログラムを作る上で作業上のオーバーヘッドを十分に減らしつつ、実行時に大きな遅延の影響を受けることもなくなりました。現在の実装は単純なマーク・アンド・スイープのガーベジコレクタを使用していますが、現在置き換えの作業を行っています。

もう一つのポイントは、並列のマルチスレッドプログラミングの難しさの大部分を占めているのがメモリ管理だ、という点です。オブジェクトがスレッド間でやりとりされはじめると、そのオブジェクトを安全に開放できるかどうか保証するのが難しくなります。自動ガーベジコレクションを適用することで、並列プログラミングのコードを書くのが極めて簡単になります。もちろん、並列環境でのガーベジコレクションを実装するのは、それ自身、一つのチャレンジではあるのですが、この一回の努力は、全てのプログラム、すべての人を助けることになると考えています。

並列性の問題は置いておいても、ガーベジコレクタのおかげで、最終的にソースコード中のインタフェースはシンプルになっていきます。インタフェースのこちらと向こうで、メモリをどう管理するのか?というのを指定する必要がなくなるからです。

ユニコード識別子って何のことですか?

ASCIIの範囲内の空間から識別子を広げるというのは私たちにとって重要な課題です。識別子の文字はユニコードで規定されている文字か数字でなければならないというGoのルールは理解しやすく、実装も簡単ですが、制限がいくつかあります。例えば、設計上、 合字 は除外されています。識別子が何であるかという外部定義があり、さらに正規化された識別しの定義があり、曖昧でないことが保証されるまでは、合字との組み合わせは除外しておいた方が良いと思っています。このため、文字空間をひろげつつ、あいまいな識別子を認めることによって発生しうるバグを避けるためのシンプルなルールを適用しています。

関連する項目として、exportされる識別子は大文字から始まってなければならないというものがあります。そのため、大文字や小文字がない言語の場合には、定義上、識別子をexportすることはできません。現状で唯一可能な解決策は X日本語 というように定義することですが、明らかに良い方法とはいえません。私たちは現在別のオプションも検討しています。が、大文字にすると外部に見えるようになる、という識別子のルールは、私たちが気に入っているも機能でもあるので、今後も変わることはないでしょう。

Absent features

Why does Go not have generic types? Generics may well be added at some point. We don’t feel an urgency for them, although we understand some programmers do.

Generics are convenient but they come at a cost in complexity in the type system and run-time. We haven’t yet found a design that gives value proportionate to the complexity, although we continue to think about it. Meanwhile, Go’s built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.

This remains an open issue.

Why does Go not have exceptions? Exceptions are a similar story. A number of designs for exceptions have been proposed but each adds significant complexity to the language and run-time. By their very nature, exceptions span functions and perhaps even goroutines; they have wide-ranging implications. There is also concern about the effect they would have on the libraries. They are, by definition, exceptional yet experience with other languages that support them show they have profound effect on library and interface specification. It would be nice to find a design that allows them to be truly exceptional without encouraging common errors to turn into special control flow that requires every programmer to compensate.

Like generics, exceptions remain an open issue.

Why does Go not have assertions? This is answered in the general FAQ.

なぜ型の継承が無いのですか?

オブジェクト指向のプログラミングにおいて、少なくとも良く知られている言語では、ほとんどの場合自動的に導きだせる型同士の関係について、多くの議論がなされてきました。GOは別のアプローチを取ります。

2種類の型の関係を宣言する時間はプログラマには必要なく、GOでは型は自動的にそのメソッドのサブセットが指定するインタフェースを全て満たします。設計書の削減に加え、このアプローチは実際に有利に働きます。型は伝統的な多重継承の複雑さを伴わずに、一度に多くのインタフェースを満たすことが出来ます。インタフェースはそのコンセプトを表現することができる1個ないし0個のメソッドを持つことで、とても軽量化することが出来ます。インタフェースは、新しいアイデアが出てたり、元の型の注釈を付けずにテストした後に、追加することが出来ます。なぜなら、型とインタフェースの間には明確な関係がありません、管理したり議論したりするための型の階層構造は存在しないのです。

これらのアイデアで、タイプセーフなUnixのパイプと類似したものを構築することが可能になります。例えば、fmt.Fprintfがどのようにファイルだけではない様々な出力に対してフォーマットを可能にしているのか、どのようにしてbufioパッケージがfile I/Oと完璧に区別されているか、どのようにしてcryptoパッケージがブロック暗号とストリーム暗号を縫い合わせているかを見てみてください。これら全てのアイデアは、単一のメソッド(Writer)を提供する単一のインタフェース(io.Writer)に根幹をなします。表面的な部分を触っているだけなのです。

これはすこし慣れが必要かもしれませんが、この暗黙の型継承がこそがGOにおいて最もエキサイティングな点なのです。

なぜlenはメソッドではなく関数なのですか?

私たちはこの問題について議論しましたが、lenとその仲間をを関数として実装することにしました。それは、経験的に分かりやすく、基本的な型(Goにおいての型の意味)のインタフェースの問題を複雑にしないからです。

なぜGoはメソッドと演算子のオーバーライドをサポートしないのですか?

型のマッチングをする必要がない場合、メソッドの振り分けは簡素化できます。他の言語から分かることですが、同名で違う型宣言である関数は時として便利な反面、実際には混乱を招き、破綻しやすいことが有ります。型を名前のみでマッチングし、一貫性を必要とする方針は、Goの型システムを簡素化する主要な決定です。

演算子のオーバーライドについては、絶対的な要件というよりも便利機能なように思えます。何度も言うように、無い方がシンプルになるということです。

なぜGoには暗黙的な数値変換がないのですか?

C言語における数値型での自動型変換の利便性は過剰であり、混乱を招く原因となっています。式が符号無しなのはいつか? 値の大きさはどの位? オーバーフローする? 型変換の結果はポータブルで実行されるマシンに非依存? これはコンパイラも複雑にします。「通常の算術変換」の実装は簡単ではなく、アーキテクチャ間で一貫性を保つことも簡単ではありません。私たちは移植性の理由から、コード中で明示的に型変換をする労力を払うことによって、明確で解り易くする決断をしました。それでも、Go言語での定数の定義(符号およびサイズのアノテーションの要らない任意精度の値)は問題をかなり改善しています。

これと関連しますが、C言語とは違い、仮にintが64ビットだったとしても、intとint64は明確に別の型となります。int型はジェネリックです。整数が何ビットあるのかを気にしたい場合、Go言語では明示的に指定することが推奨されます。

なぜmapは組み込みなのですか?

stringと同じ理由です。これらは強力で重要なデータ構造であり、構文によるサポートがある優れた実装は、プログラミングをより快適にします。私たちはほぼすべての用途において、Go言語のmapの実装が十分に強力だと確信しています。特定のアプリケーション向けにカスタム実装が有益かもしれない場合、それを書くことはできるでしょうが、構文的に便利にはならないでしょう。妥当なトレードオフのようです。

なぜmapは構造体や配列をキーとして許可していないのですか?

mapのルックアップには等価演算子が必要となりますが、構造体および配列はこれを実装していません。実装していない理由は、このような型では等価演算子がwell-definedで無いためです。shallowあるいはdeepな比較か、ポインタあるいは値の比較か、再帰的な構造体をどう扱うか、などといった考慮すべき点が多数存在します。私たちはこの問題を再考(そして既存のプログラムを無効にすることのない構造体と配列用の等価演算子を実装)してもよいのですが、構造体や配列の等価演算子が何を意味すべきかについて明確なアイデアが無いので、いまのところ単に放置しておきます。

なぜ配列は値なのに、map、slice、channelは参照なのですか?

この話題については多くの過去があります。初期の段階では、mapおよびchannelは構文的にはポインタであり、非ポインタのインスタンスとしての宣言や使用はできませんでした。また私たちは、配列がどう動作すべきか苦心していました。最終的には、ポインタと値を厳密に分離すると、言語が使いにくくなると判断しました。配列を参照の形で扱うsliceを含めて、参照型を導入することでこれらの問題を解決しました。参照型によって、言語に残念な複雑さが加わりましたが、使い易さにおいて大きな効果がありました。これらが導入された時点で、Go言語はより生産的で快適な言語になりました。

並列処理

なぜ並列処理はCPSのアイデアを元にしたのでしょうか?

並列処理やマルチスレッドのプログラミングは難しいと言われています。私たちはこの問題がpthreadなどの複雑な設計や、mutex、条件変数、果てはメモリバリアといった低位レベルの詳細が強調され過ぎていることに、ある程度起因すると思っています。mutexなどが依然としてその裏に隠れていたとしても、高位レベルのインタフェースはコードをずっと単純にします。

言語上で高位レベルな並列処理のサポートを提供するモデルの最たる成功例に、HoareのCommunicating Sequential Processes(CSP)があります。OccamおよびErlangの2つは、CSPに由来するよく知られた言語です。Go言語の並列処理プリミティブは、これらと違う系統に由来するものであり、そのメインの提案はチャネルをファーストクラスオブジェクトとする強力な概念です。

なぜスレッドではなくgoroutineなのでしょうか?

goroutineは並列処理を使い易くします。少し前からあたためていたそのアイデアとは、独立に実行する複数の関数(コルーチン)を、スレッドの集合に多重化することでした。ブロッキングするシステムコールを呼んだ場合などでコルーチンがブロックされる際に、ランタイムは同一スレッドにある他のコルーチンたちを別の実行可能なスレッドに自動的に移動して、それらがブロックされないようにします。プログラマはこの場面を見ることはありませんが、これこそが重要なのです。私たちがgoroutineと呼ぶこの仕組みは、非常に軽い処理にすることができます。実行時間の長いシステムコールで長時間費やさなければ、そのコストはスタック用のメモリ処理にかかるものより少し多い程度で済みます。

スタックを小さくするため、Go言語のランタイムはセグメント化されたスタックを使います。新しい出来立てのgoroutineでは数キロバイトが割り当てられますが、それはほとんどの場合で充分な大きさです。充分でない場合でも、ランタイムは追加でセグメントを自動的に割り当て(そして解放)します。関数呼び出しごとのオーバヘッドは、処理の軽い命令3つ分ぐらいが平均的なものです。同一アドレス空間で数十万規模のgoroutineを生成できるほど実用的なのです。もしgoroutineが単なるスレッドであったら、システムリソースはもっと小さな規模で枯渇してしまったでしょう。

なぜmapの処理はアトミックとして定義されていないのでしょうか?

長い議論の末、mapの典型的な使用では複数スレッドからのアクセスを安全に保護する必要性は無いと判断しました。そのような状況では、mapはほぼ確実に大きなデータ構造か計算の一部であり、すでに同期されています。そのためmapの処理すべてにおいてmutexを獲得すると、ほとんどのプログラムでスローダウンするものの、安全性の向上はあまり期待できません。しかしながら、mapへの制御されていないアクセスはプログラムをクラッシュさせ得ることを意味するため、簡単な議論ではありませんでした。

言語仕様上ではアトミックなmapの更新を排除していません。信頼できないプログラムを稼動させるなど、その必要性がある場合、mapへのアクセスを実装によって安全のためにインターロックすることも可能です。