Skip to content

Go公式のFAQを日本語訳したものです

Notifications You must be signed in to change notification settings

toshi0607/Go-FAQ-Japanese-Edition

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

274 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Go FAQ Japanese Edition

Frequently Asked Questions (FAQ)の日本語版です。

目次

起源

プロジェクトの目的は?

Goが誕生した2007年当時、プログラミングの世界は現在と異なっていました。プロダクションソフトウェアは、通常C++かJavaで書かれており、GitHubは存在せず、ほとんどのコンピュータはまだマルチプロセッサではなく、Visual StudioとEclipse以外にIDEやその他のハイレベルなツールはほとんどなく、ましてやインターネット上で無償で利用できるものはほとんどありませんでした。

その一方で、使用していた言語とそれに関連するビルドシステムで大規模なソフトウェアプロジェクトを構築するのに必要な過度の複雑さに苛立ちを覚えていました。C、C++、Javaなどの言語が開発されて以来、コンピュータは劇的に速くなりましたが、プログラミング行為自体はそれほど進歩しませんでした。また、マルチプロセッサが一般的になりつつあることは明らかでしたが、ほとんどの言語には、それらで効率的かつ安全にプログラムするためのサポートはほとんどありませんでした。

技術の発展がするにつれて、今後ソフトウェア工学の主要な課題になっていくのは何か、新しい言語はその解決にどう役立てられるかを一歩踏み込んで考えてみることにしました。たとえば、マルチコアCPUの台頭により、言語はある種の並行処理や並列処理を第一級にサポートする必要が生じます。また、大規模な並列プログラムにおいてリソース管理をしやすくするために、ガベージコレクションや、少なくともある種の安全な自動メモリ管理が必要とされます。

このような検討から、最初はアイデアと要望のセットとして、次に言語として、Goが生まれるまでの一連の議論が始まりました。Goは、ツール化、コードフォーマットなどの雑務の自動化、大規模なコードベースでの作業に対する障害の除去など、現役プログラマーの支援に力を入れることを包括的な目標として掲げました。

Goの目標とその達成方法、少なくともそのアプローチについては、Go at Google: Language Design in the Service of Software Engineeringという記事で、より広範な説明を見ることができます。

プロジェクトの歴史は?

2007年9月21日、Robert Griesemer、Rob Pike、Ken Thompsonは、新しい言語の目標をホワイトボードに書き始めました。数日のうちに、目標は何かをする計画や中身の名案に落ち着きました。設計は、別の仕事と並行してパートタイムで継続しました。2008年1月までに、Kenはコンパイラに取り掛かり、アウトプットとしてCのコード生成するというアイディアを模索していました。2008年半ばには、その言語はフルタイムのプロジェクトとなり、プロダクションのコンパイラを試すのに十分な水準に達していました。2008年5月、Ian Taylorが独自に、ドラフト仕様を使ってGo向けのGCCフロントエンドの開発を開始しました。2008年後半にはRuss Coxが参画し、言語とライブラリをプロトタイプから実用に耐えるのものにするのをサポートしました。

Goは、2009年11月10日にオープンソースプロジェクトとして公開されました。コミュニティから数え切れないほどの人々がアイデア、議論、コードに貢献してきました。

現在、世界に何百万人ものGoプログラマ(ゴーファー)がおり、日々増えています。Goの成功は、私たちの期待をはるかに超えるものでした。

ゴーファーマスコットの起源は?

マスコットとロゴは、Renée French(ルネ・フレンチ)がデザインしました。Plan 9のうさぎ、Glendaもデザインしています。ゴーファーについてのブログ記事では、何年か前にWFMUのTシャツのデザインに使ったものが元になっていると説明されています。ロゴとマスコットは、クリエイティブ・コモンズ3.0ライセンスで保護されています。

ゴーファーには、特徴と正しい表現方法を説明したモデルシートがあります。このモデルシートは、2016年のGopherconでのRenéeによる講演で初めて公開されたものです。彼には独自の特徴があります。ただのゴーファー(ホリネズミ)ではなく、Goのゴーファーなのです。

言語はGoと呼ぶ?それともGolang?

この言語はGoと呼ばれています。「golang」というあだ名は、もともとWebサイトがgolang.orgだったからです(当時は.devドメインはありませんでした)。そうは言うものの、多く人がgolangという名前を使っており、ラベルとして便利です。たとえば、ソーシャルメディアのハッシュタグは「#golang」です。それでも言語の名前は、単純にGoです。

余談として、公式ロゴでは大文字を2つ並べていますが、言語名はGOではなく、Goです。

なぜ新しい言語を作った?

Goは、Googleでの仕事において、既存の言語や環境に対する不満から生まれました。プログラミングはあまりにも難しくなっており、言語の選択もその一因でした。効率的なコンパイル、効率的な実行、プログラミングのしやすさからどれかを選ばなければならず、同じ主流な言語ではこの3つすべては得られませんでした。安全性や効率性よりも手軽さを重視するプログラマーは、C++やJavaよりも、PythonやJavaScriptといった動的型付け言語に移っていきました。

このような懸念を抱いていたのは、私たちだけではありませんでした。プログラミング言語に長年大きな変化がありませんでしたが、Goは、Rust、Elixir、Swiftなど、プログラミング言語の開発を活発で主流な分野の地位に再び押し上げた、いくつかの新しい言語の最初の1つでした。

Goは、インタプリタ型の動的型付け言語のプログラミングの容易さと、静的型付けコンパイル言語の効率性と安全性を組み合わせることで、これらの問題に対処しました。また、ネットワークやマルチコアコンピューティングをサポートする現代的な言語であることも目指しました。単一のコンピュータで大きな実行ファイルをビルドするのにかかる時間は、せいぜい数秒です。これらの目標を達成するため、私たちは現在の言語のプログラミングアプローチをいくつか見直すことになりました。階層型ではなく構成型の型システム、並行処理とガベージ・コレクションのサポート、依存関係の厳格な指定などでです。これらは、ライブラリやツールではうまく対応できず、新しい言語が必要とされたのです。

Go at Googleという記事では、Go言語の設計の背景とモチベーションについて議論し、このFAQで紹介されている多くの回答についてより詳細に説明しています。

Goの先祖は?

Goは、ほとんどがC言語系(基本構文)で、Pascal/Modula/Oberon系(宣言、パッケージ)からの影響が大きく、さらにNewsqueakやLimbo(並行処理)など、Tony HoareのCSPに影響を受けた言語からのアイデアも含まれています。しかし、全面的に新しい言語です。あらゆる面で、プログラマが何をするのか、どうすればプログラミング(少なくとも私たちが行うようなプログラミング)をより効果的に、つまりより楽しくできるのかを考えて設計された言語なのです。

設計における指針は?

Goが設計された当時、少なくともGoogleでは、サーバを書くのにJavaとC++が最もよく使われていた言語でした。私たちは、これらの言語では簿記のような記述と繰り返しが多すぎると感じていました。プログラマーの中には、効率と型安全性を犠牲にして、Pythonのようなより動的で流動性な言語へと移行した人もいました。私たちは、効率と安全性と流動性を一つの言語で実現できるはずだと考えました。

Goは、どちらの意味でもタイピングの量を減らすことを試みています。設計全体を通して、私たちは煩雑さと複雑さを減らすように努めました。前方宣言やヘッダーファイルはなく、すべてが一度だけ宣言されます。初期化は表現力豊かで、自動的で、簡単に使えます。文法はすっきりしており、キーワードも少ないです。繰り返し(foo.Foo* myFoo = new(foo.Foo))も、:= 宣言・初期化構造を使ったシンプルな型派生によって軽減されています。そして、おそらく最も革新的なのは、型の階層がないことです。型はただ存在するだけで、その関係を表明する必要はありません。これらの単純化により、Goは生産性を犠牲にすることなく、表現力豊かでありながら理解しやすいものとなっています。

もう一つの重要な原則は、概念の直行性を維持することです。メソッドは任意の型に実装できる、構造体はデータを表し、インターフェースは抽象化を表す、などです。直交性を保つことで、物事が組み合わさったときに何が起こるかを理解しやすくなります。

利用状況

Googleは社内でGoを利用している?

利用しています。GoはGoogleの内部の製品で広く使われています。例のひとつは、Googleのダウンロードサーバー(dl.google.com)で、Chromeバイナリや、apt-getパッケージのような大規模なインストールファイルを配信しているというものです。

GoはGoogleで使用される唯一の言語ではありませんし、他にもたくさんの言語を使用しています。しかし、サイト信頼性エンジニアリング(SRE)や大規模データ処理など、多くの分野で重要な言語となっています。また、Google Cloudを実行するソフトウェアの中核でもあります。

他にGoを利用している企業は?

Goの利用は世界中で増加しており、特にクラウドコンピューティングの領域で顕著です。Goで書かれた主要なクラウドインフラプロジェクトには、DockerとKubernetesがありますが、他にもたくさんあります。

クラウドだけではありません。go.devのウェブサイトに掲載されている企業や成功事例を見れば明らかです。また、Go Wikiには定期的に更新されるページがあり、Goを利用している多くの企業のうちの一部のリストが掲載されています。

また、Wikiには、この言語を使っている企業やプロジェクトの更なる成功事例へのリンクページがあります。

Goのプログラムは、C/C++のプログラムとリンクできる?

CとGoを同一アドレス空間で一緒に使うことは可能ですが、自然な形ではなく、インターフェースとなる特別なソフトウェアが必要になることがあります。また、C言語とGoのコードをリンクすると、Goが提供するメモリ安全性とスタック管理特性が失われます。問題を解決するためにCライブラリを使用することが絶対に必要な場合がありますが、純粋なGoコードにはないリスク要素が常に発生するため、注意して行ってください。

GoでCを使用する必要がある場合、進め方はGoコンパイラの実装に依存します。GoチームがサポートしているGoコンパイラの実装は3つあります。デフォルトのコンパイラーであるgc、GCCバックエンドを使用する gccgo、そしてLLVMインフラストラクチャを使用する、やや成熟度の低いgollvmです。

gcは、Cとは異なる呼び出し規則とリンカを使用しているため、Cプログラムから直接呼び出すことはできませんし、その逆も同様です。cgoプログラムは、GoコードからCライブラリを安全に呼び出せるようにするための「他言語関数インターフェース」のメカニズムを提供します。SWIGは、この機能をC++ライブラリにも拡張しています。

cgoとSWIGをGccgogollvmと一緒に使うこともできます。これらのコンパイラは伝統的なAPIを使っているので、これらのコンパイラのコードを GCC/LLVM でコンパイルされたCやC++プログラムと直接リンクすることも、細心の注意を払えば可能です。しかし、安全にそうするには、関係するすべての言語の呼び出し規約を理解することと、GoからCやC++を呼び出すときのスタックの制限に気を配ることが必要です。

GoをサポートするIDEは?

GoプロジェクトにはカスタムIDEはありませんが、言語とライブラリはソースコードの解析が容易になるように設計されています。その結果、ほとんどの有名なエディタとIDEは、直接またはプラグインを通じて、Goを十分にサポートしています。

Goをサポートしている有名なIDEやエディタには、Emacs、Vim、VS Code、Atom、Eclipse、Sublime Text、IntelliJ(GoLandというカスタム版を通して)、その他多数が含まれます。あなたのお気に入りの環境は、Goでプログラミングするのに生産的なものである可能性が高いです。

GoはGoogleのプロトコルバッファをサポートしている?

必要なコンパイラプラグインやライブラリは、別のオープンソースプロジェクトが提供しています。github.com/golang/protobuf/ で公開されています。

設計

Goにはランタイムがある?

Goにはruntimeと呼ばれる広範なライブラリがあり、すべてのGoプログラムの一部となっています。ランタイムライブラリは、ガベージコレクション、並行処理、スタック管理、Go言語のその他の重要な機能を実装しています。ランタイムは言語の中心的な存在ですが、GoのランタイムはCのライブラリであるlibcに準じています。

しかし、Goのランタイムには、Javaランタイムが提供するような仮想マシンは含まれていないことを理解することが重要です。Goのプログラムは事前にネイティブのマシンコードにコンパイルされます(別実装ではJavaScriptやWebAssemblyにコンパイルされます)。したがって、「ランタイム」という言葉はプログラムが実行される仮想環境を表すためによく使われますが、Goでは重要な言語サービスを提供するライブラリにつけられた名前にすぎません。

Unicodeの識別子はどうなっている?

Goを設計する際、ASCIIに過度に依存しないようにしたいと考えました。つまり、7ビットASCIIの制約から識別子の空間を拡張するということです。識別子文字はUnicodeで定義された文字か数字でなければならないというGoのルールは、理解も実装も簡単ですが制約があります。たとえば、結合文字は設計上除外されており、デーヴァナーガリーなど一部の言語は除外されています。

このルールには、もう一つ残念な結果があります。エクスポートされる識別子は大文字で始まらなければならないので、ある言語の文字から作られた識別子は、定義上エクスポートすることができないのです。今のところ、X日本語のようなものを使うのが唯一の解決策ですが、明らかに満足できるものではありません。

Goの初期のバージョンから、他の言語を使用するプログラマに配慮し、識別子空間をどのように拡張するのがベストなのかかなり検討されてきました。まさにまだ議論の余地があり、将来のバージョンでは識別子の定義がより自由になるかもしれません。たとえば、Unicodeが推奨する識別子の考え方を採用することも考えられます。いずれにしても、Goの人気のある特徴の1つである、識別子の可視性を文字の大小で決定する方法を維持(あるいは拡張)しながら、互換性を保つようにする必要があります。

現時点では、プログラムを壊さずに後から拡張できるシンプルなルールを採用しています。それにより、曖昧な識別子を認めるルールであれば確実に発生してしまうバグを回避することができます。

GoにはなぜXという機能がない?

どの言語にも目新しい機能があり、誰かが好きな機能は省かれています。Goは、プログラミングのしやすさ、コンパイルの速さ、概念の直交性、並行処理やガベージコレクションなどの機能をサポートする必要性を大事にして設計されています。あなたの好きな機能は、コンパイルの速度や設計の明快さに影響するため、あるいは基本的なシステムモデルを難しくしすぎるため、省かれているかもしれません。

Goに機能Xがないことが気に入らない場合、私たちを許してGoにある機能を調査してみてください。Xがないのを面白い方法で補っていることに気づくかもしれません。

Goはいつジェネリック型が使えるようになった?

Go 1.18リリースでは、言語に型パラメータが追加されました。これにより、ポリモーフィックやジェネリックプログラミングの一種が可能になります。詳しくは、言語仕様提案をご覧ください。

なぜGoは最初ジェネリック型なしでリリースされた?

Goは、長期にわたって保守しやすいサーバプログラムを書くための言語として設計されました(背景はこの記事を参照)。設計では、スケーラビリティ、可読性、並行性などに重点が置かれました。ポリモーフィックプログラミングは当時、この言語の目標に不可欠とされませんでした。そのため、最初はシンプルさを守るために省かれたのです。

ジェネリックは便利ですが、型システムおよびランタイムの複雑さという代償を伴います。複雑さに比例した価値を与えてくれると信じられる構想を練るのに時間がかかりました。

なぜGoには例外処理機構がない?

try-catch-finallyイディオムのように例外を制御構造に結びつけると、コードが複雑になると考えています。また、ファイルを開けなかったというような普通のエラーを例外として扱うようプログラマに促す傾向があります。

Goは異なるアプローチを取ります。単純なエラー処理では、Goの複数値の戻り値によって、戻り値をオーバーロードせずにエラーを報告することが簡単にできます。Goの標準的なエラー型を利用すると、他の機能と相まって、エラー処理は快適に行えますが他の言語での処理とはかなり異なっています。

Goには、本当に例外的な状態を通知し、そこから回復するための組み込み関数もいくつかあります。回復機構は、エラー後に関数の状態が破棄される際にのみ実行されます。これは大惨事を処理するには十分ですが、余分な制御構造は必要なく、うまく使えばきれいなエラー処理コードになります。

詳しくは、Defer, Panic, and Recoverという記事をご覧ください。また、Errors are valuesというブログ記事では、エラー処理にGoの力が遺憾なく発揮されていることを示し、Goできれいにエラー処理するためのアプローチを説明しています。エラーは単なる値なのです。

なぜGoにはアサーションがない?

Goはアサーションを提供しません。アサーションは紛れもなく便利ですが、私たちの経験では、プログラマは適切なエラー処理とレポートを考えるのを避けるために、それを松葉杖として使っています。適切なエラー処理とは、致命的でないエラーが発生しても、サーバーがクラッシュせずに動作を継続することです。適切なエラー報告とは、エラーが直接的で的確であることを意味し、プログラマは膨大なクラッシュトレースを解釈する手間が省けます。プログラマーがコードに精通していない場合、正確なエラーは特に重要です。

私たちは、これが争点になることを理解しています。Go言語とライブラリには、現代の慣行とは異なるものがたくさんあります。それは単に、異なるアプローチを試してみる価値があると感じたからです。

なぜCSPのアイデアの上に並行処理を構築する?

並行処理とマルチスレッドプログラミングは、長い間、難しいという評価を受け続けてきました。これは、pthreadsのような複雑な設計や、ミューテックス、条件変数、メモリバリアなどの低レベルの詳細が過度に強調されていることが原因だと考えています。高レベルのインタフェースがあることで、たとえその下にまだミューテックスなどがあるとしても、 よりシンプルなコードになります。

並行処理のための高レベルの言語サポートを提供する最も成功したモデルの1つは、HoareのCommunicating Sequential Processes(CSP)に由来します。OccamとErlangは、CSPから派生した2つの有名な言語です。Goの並行処理プリミティブはHoareのCSPから派生したもので、主な成果はチャンネルをファーストクラスのオブジェクトとして扱うという強力な概念です。以前のいくつかの言語での経験から、CSPモデルは手続き型言語のフレームワークにうまく当てはまることが分かっています。

なぜスレッドではなくゴルーチン?

ゴルーチンは、並行処理を簡単にするためのものです。このアイデアは以前からあり、独立して実行される関数(コルーチン)をスレッドの集合に多重化することです。コルーチンがブロッキングシステムコールを呼び出すなどしてブロックされると、ランタイムは自動的に同じOSスレッド上の他のコルーチンを別の実行可能なスレッドに移動させ、ブロックされないようにします。プログラマーにはこのようなことには気づかず、それが重要な点です。その結果としてのゴルーチンはとても安価です。スタック用のメモリ(数キロバイト)以上のオーバーヘッドはほとんどありません。

スタックを小さくするために、Goのランタイムはサイズ変更可能で境界のあるスタックを使用します。新しく作成されたゴルーチンには数キロバイトが与えられますが、これはほとんど常に十分な量です。足りないときは、ランタイムがスタックを格納するメモリを自動的に拡大(と縮小)させるので、多くのゴルーチンが適度な量のメモリで生存できるようになります。CPUのオーバーヘッドは、関数呼び出し1回につき平均して3個程度の安い命令です。同じアドレス空間に何十万ものゴルーチンを作ることは実務上可能です。もしゴルーチンが単なるスレッドだったら、もっと少ない数でシステムリソースが枯渇してしまうでしょう。

なぜマップ演算はアトミックに定義されていない?

長い議論の末、マップの典型的な使い方では複数のゴルーチンから安全にアクセスする必要はなく、アクセスする場合でも、マップはすでに同期されている大きなデータ構造または計算の一部である可能性が高いと判断されました。そのため、すべてのマップ操作にミューテックス取得を要求すると、ほとんどのプログラムの速度を低下させ、安全性にはほとんど寄与しません。しかし、これは制御不能なマップアクセスがプログラムをクラッシュさせる可能性があることを意味するので、簡単な決定ではありませんでした。

Goはアトミックなマップ更新を排除しているわけではありません。信頼されていないプログラムをホストする場合など、必要であれば、実装はマップアクセスをインターロックすることができます。

マップへのアクセスが安全でないのは、更新時のみです。すべてのゴルーチンがマップの要素を読み取るだけ(for range ループを使用してマップをイテレーションすることを含め、マップの要素を参照する)だけで、要素への割当や削除によってマップを変更しなければ、同期せずに同時にマップにアクセスしても安全です。

マップを正しく使用するための補助として、Goのいくつかの実装には、並行実行によってマップが安全でない方法で変更された場合に、実行時に自動的に報告する特別なチェック機能があります。

言語の変更提案は受け入れられる?

Goの改善はよく提案され、そういった議論の歴史はメーリングリストで確認できます。しかし、それらの変更はほとんど受け入れられてきませんでした。

Goはオープンソースのプロジェクトですが、言語とライブラリは、少なくともソースコードレベルで、既存のプログラムを壊すような変更を防ぐ互換性の約束によって守られています(最新の状態に保つために、プログラムは時々再コンパイルする必要があるかもしれません)。提案がGo 1の仕様に違反する場合、その利点にかかわらず、そのアイデアを受け入れられません。将来のメジャーリリースは、Go 1と互換性がないかもしれませんが、このトピックに関する議論は始まったばかりです。1つ確かなことは、その過程で非互換性が生じることはほとんどないということです。さらに、互換性の約束は、そのような状況が発生した場合に古いプログラムが自動で適応する手段を提供することを後押ししてくれます。

変更提案がGo 1の仕様と互換性があったとしても、Goの設計目標の精神にはそぐわないかもしれません。Go at Google: Language Design in the Service of Software Engineeringという記事では、Goの起源とその設計の動機を説明しています。

Goはオブジェクト指向の言語?

イエスでもありノーでもあります。Goには型とメソッドがあり、オブジェクト指向のプログラミングが可能ですが、型の階層はありません。Goの「インターフェース」の概念は、使いやすく、ある意味より一般的だと思われる別のアプローチを提供します。また、型を他の型に埋め込むこともでき、サブクラス化と似ているが同一ではないものを提供します。さらに、GoのメソッドはC++やJavaよりも一般的で、あらゆる種類のデータに対して定義することができ、「ボックス化されていない」整数のような組み込み型に対しても可能です。メソッドは構造体(クラス)に限定されるものではありません。

また、型階層がないため、Goの「オブジェクト」は、C++やJavaなどの言語よりもずっと軽量に感じられます。

メソッドの動的ディスパッチを行うには?

動的にディスパッチされるメソッドを持つ唯一の方法は、インターフェースを介することです。構造体やその他の具象型に対するメソッドは、常に静的に解決されます。

なぜ型の継承がない?

オブジェクト指向プログラミングは、少なくとも最もよく知られた言語では、型と型の間の関係についてあまりに多くの議論を必要とし、その関係が自動的に導き出されることがよくあります。Goは異なるアプローチを取ります。

Goでは、2つの型が関連していることをプログラマーが前もって宣言するのではなく、型のもつメソッドのサブセットを指定するインターフェースが自動的に満たされます。このアプローチには、帳簿をつける手間を省けるだけでなく、実際の利点もあります。型は一度に複数のインターフェースを満たすことができ、従来の多重継承のような複雑なことはありません。インターフェースは非常に軽量です。メソッドが1つのインターフェースでも、メソッドがないインターフェースでさえも、役立つ概念を表現できます。新しいアイデアやテストのために、元の型にアノテーションつけることなく、後からインターフェースを追加することができます。型とインターフェースの間には明示的な関係がないので、型階層を管理したり、議論したりする必要はありません。

これらのアイデアを利用して、型安全なUnixパイプに類似したものを構築することが可能です。たとえば、fmt.Fprintfがファイルだけでなくあらゆる出力に対してフォーマットされたプリントを可能にする方法、bufioパッケージがファイルI/Oから完全に分離できる方法、imageパッケージが圧縮画像ファイルを生成する方法などをご覧ください。これらのアイデアはすべて、単一のメソッド(Write)を表す単一のインターフェース(io.Writer)に由来しています。そして、これはほんの一部に過ぎません。Goのインターフェースは、プログラムがどのように構造化されるかに大きな影響を及ぼします。

慣れるまで少し時間がかかりますが、この暗黙の型依存のスタイルは、Goの最も生産的なものの1つです。

なぜlenはメソッドではなく、関数?

私たちはこの問題について議論しましたが、lenとその仲間を関数として実装することは実際に問題はなく、基本型のインターフェース(Goの型の意味で)についての質問を複雑にすることもないと判断しました。

なぜGoはメソッドや演算子のオーバーロードをサポートしない?

型マッチングも行う必要なければ、メソッドディスパッチは単純化されます。他の言語での経験から、同じ名前でシグネチャが異なるさまざまなメソッドがあると便利なこともありますが、実際には混乱しやすく壊れやすいということがわかりました。名前だけでマッチングし、型における一貫性を要求することは、Goの型システムにおける主要な単純化の決定でした。

演算子のオーバーロードについては、絶対条件というより、利便性の方が高いように思います。繰り返しになりますが、演算子のオーバーロードはないほうがシンプルです。

なぜGoには「implements」宣言がない?

Goの型は、インターフェースのメソッドを実装するだけでそのインターフェースを満たします。この特性により、既存のコードを変更することなくインターフェースを定義し使用することができます。構造的な型付けが可能になるので、関心事の分離が促進され、コードの再利用が向上し、コードが増えるにつれて出現するパターンに基づいて構築することが容易になります。インターフェースのセマンティクスは、Goの軽快さ、軽量さの主な理由の一つです。

詳しくは、型の継承に関する質問をご覧ください。

実装した型がインターフェースを満たしていることを保証するには?

TがインターフェースIを実装しているかどうかは、Tのゼロ値やTへのポインタを適宜使って代入を試みることでコンパイラに確認させることができます。

type T struct{}
var _ I = T{}       // 型TがインターフェースIを実装していることの確認
var _ I = (*T)(nil) // 型*TがインターフェースIを実装していることの確認

T(または*T)がIを実装していない場合、その間違いはコンパイル時に検出されます。

あるインターフェースのユーザがそのインターフェースを実装していることを明示的に宣言したい場合、説明的な名前のメソッドをインターフェースのメソッドセットに追加することができます。

例:

type Fooer interface {
    Foo()
    ImplementsFooer()
}

型がFooerであるためには、ImplementsFooerメソッドを実装し、go docの出力でその事実を明確に文書化し、アナウンスする必要があります。

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

ほとんどのコードはこのような制約を利用しません。なぜなら、このような制約はインターフェースのアイデアの有用性を制限してしまうからです。しかし、類似したインターフェース間の曖昧さを解消するために制約が必要になることもあります。

なぜT型はEqualインターフェースを満たさない?

自分自身と他の値を比較することができるオブジェクトを表現する簡単なインターフェースを考えてみましょう。

type Equaler interface {
    Equal(Equaler) bool
}

そしてこれがT型とします。

type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler

いくつかの多相型システムにおける同様の状況とは異なり、TEqualerを実装していません。T.Equalの引数の型はTであり、文字通り必須の型であるEqualerではありません。

Goでは、型システムはEqualの引数を積極的にはサポートしません。Equalerを実装しているT2型が示すように、これはプログラマの責任です。

type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) }  // satisfies Equaler

これは他の型システムとは異なります。Goでは、Equalerを満たすあらゆる型がT2.Equalの引数として渡され、実行時にその引数がT2型であることをチェックしなければならないためです。言語によっては、コンパイル時にその保証をするようにアレンジしているものもあります。言語によっては、コンパイル時にその保証をするように取り計らっているものもあります。

関連する例として、逆のケースもあります。

type Opener interface {
   Open() Reader
}

func (t T3) Open() *os.File

他の言語では満たすかもしれませんが、GoではT3Openerを満たしません。

このような場合、Goの型システムはプログラマにとってあまり多くをサポートしてくれないのは事実です。しかし、サブタイピングがないため、インターフェースの充足性に関するルールはつぎのように非常に簡単に記述できます。関数の名前とシグネチャはインターフェースのものと全く同じか?また、Goのルールは効率的に実装するのが簡単です。これらの利点は、自動的な型の上位変換(type promotion)がないことを補って余りあるものだと感じています。

[]Tを[]interface{}に変換できる?

直接はできません。この2つの型はメモリ上で同じ表現を持っていないため、言語仕様で禁止されています。変換先のスライスに個別に要素をコピーする必要があります。この例では、intのスライスをinterface{}のスライスに変換しています。

t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}

T1とT2が同じ基底型を持つとき[]T1を[]T2に変換できる?

このコードサンプルの最後の行は、コンパイルできません。

type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK

Goでは、型はメソッドと密接に結びついており、名前のついた型はすべて(おそらく空の)メソッドセットを持っています。原則的には、変換される型の名前は変更できますが(したがって、そのメソッドセットも変更できます)、複合型の要素の名前(とメソッドセット)は変更できません。Goでは、型の変換について明示的である必要があります。

なぜnilエラー値はnilと等しくない?

内部的には、インターフェースはT型と値Vの2つの要素で実装されています。Vint、構造体、ポインタなどの具体的な値であり、決してインターフェースそのものではありません。また、T型を持ちます。たとえば、あるインターフェースにint型の値3を格納すると、できあがるインターフェースの値は、模式的に、(T=int, V=3)となります。あるインターフェース変数がプログラムの実行中に異なる値V(とそれに対応する型T)を保持する可能性があるため、値Vはインターフェースの動的値とも呼ばれます。

インターフェースの値がnilであるのは、VTがともに未設定である場合(T=nilVは未設定)だけです。特に、nilインターフェースは常にnil型を保持することになります。int型のnilポインタをインターフェース値の内部に格納すると、ポインタの値に関係なく内部の型は*intになります (T=*intV=nil)。したがって、このようなインターフェース値は、内部のポインタ値Vnilであっても、非nilになります。

この状況は紛らわしいです。エラーリターンなど、インターフェース値の内部にnil値が格納されている場合に発生します。

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

エラーを返す関数では、エラーが正しく作成されることを保証するために、*MyErrorのような具象型ではなく、(上で行ったように)常にerror型をそのシグネチャで使用することは良いアイデアです。例として、os.Openは、nilでない場合常に具象型*os.PathErrorであるにもかかわらず、errorを返します。

ここで説明したのと同じような状況は、インターフェースを使うときにはいつでも起こり得ます。ただ、インターフェースに具体的な値が格納されている場合、インターフェースはnilにならないことを覚えておいてください。詳しくは、「The Laws of Reflection」を参照してください。

なぜCのようにタグなしのunionがない?

タグなしのunionは、Goのメモリ安全保証に違反します。

なぜGoにはバリアント型がない?

バリアント型は代数型とも呼ばれ、ある値が他の型の集合のうちいずれかを取るかもしれないが、それらの型だけを取るということを指定する手段を提供します。システムプログラムの一般的な例では、エラーがネットワークエラー、セキュリティエラー、アプリケーションエラーであることを指定し、呼び出し側がエラーの型を調べることによって問題の原因を区別できるようにします。また、構文木では、各ノードが宣言、文、代入など異なる型になることがあります。

私たちはGoにバリアント型を追加することを検討しましたが、議論の結果、インターフェースと紛らわしい形で重複するので、省くことにしました。もし、バリアント型の要素自体がインターフェースだったらどうなるでしょうか?

また、バリアント型が扱うものは、すでにGoでカバーしているものもあります。エラーの例は、エラーを保持するインターフェース値と、ケースを区別するための型スイッチを使うことで簡単に表現できます。構文木の例も、それほどエレガントではないですが実現可能です。

なぜGoには共変戻り値型がない?

共変戻り値型は、つぎのようなインターフェースです。

type Copyable interface {
    Copy() interface{}
}

つぎのメソッドで満たされます。

func (v Value) Copy() Value

なぜなら、Valueが空のインターフェースを実装しているからです。Goでは、メソッドの型は正確に一致しなければならないので、ValueCopyableを実装していません。Goでは、型が何を行うかという概念(メソッド)と型の実装を分離しています。2つのメソッドが異なる型を返した場合、それらは同じことを行っているわけではありません。共変戻り値型を求めるプログラマは、インターフェースを通して型階層を表現しようとすることがよくあります。Goでは、インターフェースと実装をきれいに分離する方が自然です。

なぜGoは暗黙的な数値変換を提供しない?

C言語における数値型間の自動変換の利便性は、それが引き起こす混乱に勝るとも劣らないものです。ある式が符号なしであるのはどんな場合でしょうか?値はどの程度大きいのでしょうか?オーバーフローするでしょうか?実行するマシンに関係なく、結果は移植可能でしょうか?また、コンパイラも複雑になります。「通常の算術変換」は実装が容易ではなく、アーキテクチャ間で一貫性がありません。移植性の理由から、コードで明示的に変換を行うことを代償として、物事を明確かつ単純にすることにしました。それでも、Goにおける定数の定義(符号やサイズのアノテーションのない任意の精度の値)は、問題をかなり改善しました。

それに関連して、C言語とは異なり、intが64ビット型であっても、intint64は別の型です。int型は汎用的なものです。整数が保持する何ビット数を気にするのであれば、Goは明示的に行うことを推奨します。

Goで定数はどのように機能する?

Goは異なる数値型の変数間の変換に厳格ですが、定数の扱いはもっと柔軟です。233.14159math.Piなどの文字定数は、任意の精度でオーバーフローもアンダーフローもない、一種の理想的な数値空間を構成しています。たとえば、math.Piの値はソースコード中で63箇所指定されており、この値を含む定数式はfloat64が保持できる精度を超えて保持されています。定数や定数式が変数(プログラム内のメモリ位置)に代入されたときだけ、通常の浮動小数点数の性質と精度を持つ「コンピュータ」数値となります。

また、Goの定数は型付けされた値ではなく単なる数値なので、変数よりも自由に使うことができ、厳密な変換ルールにまつわるぎこちなさを和らげることができます。つぎのような式を書いても、コンパイラから文句は言われません。

sqrt2 := math.Sqrt(2)

なぜなら、理想の数である2は安全かつ正確にfloat64に変換されてmath.Sqrtが呼び出されるからです。

Constatnsと題したブログ記事で、このトピックをより詳しく説明しています。

なぜマップは組み込みこまれている?

文字列が組み込みであるのと同じ理由です。強力で重要なデータ構造なので、優れた実装と構文的なサポートを提供することで、プログラミングをより快適にすることができます。Goのマップの実装は十分に強力で、ほとんどの用途に対応できると考えています。特定のアプリケーションでカスタム実装が役立つのであれば、それを書くことは可能ですが、構文的にはそれほど便利ではありません。というのは、合理的なトレードオフと言えるでしょう。

なぜマップではスライスをキーにできない?

マップ検索には等価演算子が必要ですが、スライスはこれを実装していません。このような型では等式がうまく定義されていないため、等式は実装されていません。浅い比較と深い比較、ポインタと値の比較、再帰的な型の扱い方など、複数の考慮事項があります。この問題は再検討されるかもしれませんし、 スライスに等式を実装しても、既存のプログラムが無効になることはありません。しかし、スライスの等式の意味がはっきりしない状況では、ひとまず放置することにしました。

Go 1では、以前のリリースとは異なり構造体と配列に対して等式が定義されているため、そのような型をマップのキーとして使用することができます。ただし、スライスにはまだ等式の定義がありません。

なぜ配列が値であるのに対し、マップ、スライス、チャンネルは参照?

その話題には多くの歴史があります。初期には、マップとチャンネルは構文上ポインタであり、非ポインタのインスタンスを宣言したり使用したりすることはできませんでした。また、配列がどのように機能すべきかという点でも、私たちは苦心しました。最終的には、ポインタと値を厳密に分けることがGoを使いにくくしていると判断しました。そこで、ポインタを共有データ構造への参照とすることで、これらの問題を解決しました。この変更により、言語が多少複雑になったのは残念ですが、使いやすさには大きな効果がありました。そのおかげでGoはより生産的で快適な言語となったのです。

コードの記述

ライブラリのドキュメントはどのように作成されている?

Goで書かれたgodocというプログラムがあります。これはソースコードからパッケージのドキュメントを抽出し、宣言やファイルなどへのリンクを持つウェブページとして提供するプログラムです。インスタンスはpkg.go.dev/stdで動いています。実際、godocgo.devにあるサイト全体を実装しています。

godocインスタンスは、表示するプログラム中のシンボルのリッチでインタラクティブな静的解析を提供するように設定することができます。詳細はここに記載されています。

コマンドラインからドキュメントにアクセスするために、goツールにはdocサブコマンドがあり、同じ情報へのテキストインターフェイスを提供します。

Goのプログラミングスタイルガイドはある?

明確なスタイルガイドはありませんが、一般的に認識されている「Goスタイル」があることは確かです。

Goは、名前付け、レイアウト、ファイル構成に関する意思決定をサポートするための規約を確立しています。Effective Goというドキュメントに、これらのトピックに関するアドバイスがあります。より直接的には、gofmtというプログラムはレイアウト規則を強制することを目的としたプリティプリンタです。解釈を可能にする通常のすべきこととすべきでないことリストの代わりになります。リポジトリにあるすべてのGoコードやオープンソースの世界の大部分のGoコードは、gofmtを通して実行されています。

Go Code Review Commentsと題された文書は、プログラマーが見落としがちなGoのイディオムの詳細についての非常に短いエッセイを集めたものです。Goプロジェクトのコードレビューを行う人々にとって、便利な参考資料です。

Goライブラリにパッチを出すにはどうしたらいい?

ライブラリのソースは、リポジトリのsrcディレクトリにあります。重要な変更を加えたい場合は、着手する前にメーリングリストでの議論をお願いします。

進め方の詳細については、ドキュメントContributing to the Go projectを参照してください。

なぜ 「go get」はリポジトリをクローンするときHTTPSを使用する?

企業では、標準的なTCPポート80(HTTP)と 443(HTTPS)のみの発信トラフィックを許可し、TCPポート9418(git)やTCPポート22(SSH)など、その他のポートでの発信トラフィックをブロックしていることがよくあります。HTTPの代わりにHTTPSを使用する場合、gitはデフォルトで証明書の検証を実施し、中間者攻撃、盗聴、改ざんに対する防御を提供します。そのため、go getコマンドは安全のためにHTTPSを使用します。

gitは、HTTPSで認証するように設定することも、HTTPSの代わりにSSHを使うように設定することもできます。HTTPSで認証するには、gitが参照する$HOME/.netrcファイルに一行を追加します。

machine github.com login *USERNAME* password *APIKEY*

GitHubアカウントの場合、パスワードは個人用アクセストークンにすることができます。

gitでは、指定したプレフィックスにマッチするURLに対してHTTPSではなくSSHを使うように設定することもできます。たとえば、GitHubへのアクセスにSSHを使うには、次の行を~/.gitconfigに追加します。

[url "ssh://git@github.com/"]
    insteadOf = https://github.com/

「go get」を使ってパッケージのバージョンを管理するには?

Goツールチェーンには、モジュールと呼ばれる、バージョン管理された関連パッケージのセットを管理するシステムが組み込まれています。モジュールはGo 1.11で導入され、1.14からは実運用で使用できるようになっています。

モジュールを使ってプロジェクトを作成するには、go mod initを実行します。このコマンドは、依存関係のバージョンを追跡するgo.modファイルを作成します。

go mod init example/project

依存関係を追加、アップグレード、ダウングレードするには、go getを実行します。

go get golang.org/x/text@v0.3.5

入門するにあたって、詳しくは「Tutorial: Create a Go module」をご覧ください。

モジュールによる依存関係の管理に関するガイドは、「Developing modules」を参照してください。

モジュール内のパッケージは、インポート互換性ルールに従い、進化しても後方互換性を維持する必要があります。

古いパッケージと新しいパッケージが同じインポートパスを持っている場合、新しいパッケージは古いパッケージと後方互換性がなければならず、 新しいパッケージは古いパッケージと後方互換性がなければなりません。

エクスポートされた名前を削除しない、タグ付けされた複合リテラルを推奨するなど、Go 1互換性ガイドラインはここで良い参考となります。異なる機能が必要な場合は、古い名前を変更するのではなく、新しい名前を追加してください。

モジュールは、セマンティックバージョニングとセマンティックインポートバージョニングでこれを体系化しています。互換性に問題がある場合、新しいメジャーバージョンでモジュールをリリースします。メジャーバージョン2以上のモジュールは、パスの一部としてメジャーバージョンのサフィックスを必要とします(/v2のように)。これはインポートの互換性ルールを維持するためです。メジャーバージョンの異なるモジュールのパッケージは個別のパスを持ちます。

ポインタとアロケーション

関数のパラメータが値で渡されるのはいつ?

C言語ファミリーのすべての言語と同様に、Goではすべて値で渡されます。つまり、関数は常に渡されたもののコピーを取得し、あたかもパラメータに値を代入する代入文があるかのようになります。たとえば、int型の値を関数に渡すとint型のコピーが作成され、ポインタの値を渡すとポインタのコピーが作成されますが、ポインタが指すデータのコピーは作成されません。(これがメソッドのレシーバにどのように影響するかは、後のセクションを参照してください)

マップ値とスライス値はポインタのように動作します。つまり、基礎となるマップまたはスライスデータへのポインタを含む記述子です。マップ値やスライス値をコピーしても、それが指しているデータはコピーされません。インターフェース値をコピーすると、そのインターフェース値に格納されているもののコピーが作成されます。インターフェース値が構造体を保持している場合、インターフェース値をコピーすると、構造体のコピーが作成されます。インターフェース値がポインタを保持している場合、インターフェース値をコピーするとポインタのコピーが作成されますが、やはりポインタが指すデータはコピーされません。

この議論は、操作のセマンティクスに関するものであることに注意してください。実際の実装では、セマンティクスを変更しない限り、コピーを回避するための最適化を適用することができます。

インターフェースへのポインタはいつ使うべき?

ほとんどありません。インターフェース値へのポインタは、インターフェース値の型を偽装して遅延評価を行うような、稀で厄介な状況でのみ発生します。

インターフェイスを期待する関数にインターフェイスの値へのポインタを渡すのはよくある間違いです。コンパイラはこのエラーについて文句を言いますが、この状況はまだ混乱を引き起こす可能性があります。なぜなら、インターフェースを満たすためにポインタが必要な場合があるからです。具体的な型へのポインタはインターフェースを満たすことができますが、 一つの例外を除いて、インターフェースへのポインタはインタフェースを満たすことができません。

つぎの変数宣言を考えてみてください。

var w io.Writer

プリント関数fmt.Fprintfは、第一引数にio.Writerを満たす値、つまり標準的なWriteメソッドを実装したものを受け取ります。次のように書くことができます。

fmt.Fprintf(w, "hello, world\n")

しかし、wのアドレスを渡すと、プログラムはコンパイルされません。

fmt.Fprintf(&w, "hello, world\n") // コンパイル時エラー

例外は、どんな値であっても(インターフェースへのポインタでさえも)、 空のインターフェイス型変数(interface{})に代入することができることです。それでも、値がインターフェースへのポインタである場合は、ほぼ間違いなく間違いであり。 混乱をきたす可能性があります。

値とポインタどちらに対してメソッドを定義すべき?

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

ポインタに不慣れなプログラマにとっては、この2つの例の区別がわかりにくいかも知れませんが、実は状況は非常に単純なのです。ある型にメソッドを定義するとき、レシーバ(上の例ではs)はあたかもメソッドの引数であるかのようにふるまいます。レシーバを値として定義するかポインタとして定義するかという問いは、関数の引数を値とするかポインタとするかという問いと同じです。考慮すべき点がいくつかあります。

まず、最も重要なことは、そのメソッドがレシーバを変更する必要があるかどうかです。もしそうなら、レシーバはポインタでなければなりません(スライスとマップは参照として動作するので話はもう少し複雑ですが、たとえばメソッドでスライスの長さを変更する場合、レシーバはやはりポインタでなければなりません)。上の例では、pointerMethodsのフィールドを変更すると呼び出し元にその変更が見えます。しかし、valueMethodは呼び出し元の引数のコピーで呼ばれるので(これが値を渡すという定義です)、その変更は呼び出し元からは見えません。

ところで、Javaではメソッドのレシーバは常にポインタですが、そのポインタの性質は多少偽装されています(言語に値のレシーバを追加する提案もあります)。Goの値音レシーバが変わっているのはその点です。

2つ目は、効率性の観点です。レシーバが大きい場合、たとえば大きなstructであれば、ポインタのレシーバを使った方がはるかに低コストです。

次に一貫性です。もしその型のメソッドのいくつかがポインタのレシーバを持たなければならないなら、残りのメソッドもそうすべきです。そうすれば、その型がどのように使われるかに関わらず、メソッドセットに一貫性が生まれます。詳しくはメソッドセットのセクションをご覧ください。

基本型、スライス、小さなstructなどの型では、値のレシーバは非常に安価なので、メソッドのセマンティクスがポインタを必要としない限り、値のレシーバが効率的で明確です。

newとmakeの違いは?

一言で言えば、newでメモリを確保し、makeでslice型、map型、channel型を初期化します。

詳しくは「Effective Go」の該当箇所をご覧ください。

64ビットマシンでのintの大きさは?

intuintのサイズは実装に依存しますが、あるプラットフォームでは互いに同じです。移植性のために、値の特定のサイズに依存するコードは、int64のような明示的なサイズの型を使用する必要があります。32ビットマシンでは、コンパイラはデフォルトで32ビット整数を使いますが、64ビットマシンで整数は64ビットです。(歴史的には必ずしもそうではありませんでした)。

一方、浮動小数点スカラーや複素数型は常に大きさが決まっています(floatや複素数の基本型はありません)。プログラマが浮動小数点数を使うときに精度を意識しなければならないためです。(型付けされていない)浮動小数点数定数に使われるデフォルトの型はfloat64です。したがって、foo := 3.0float64型の変数fooを宣言していることになります。(型付けされていない)定数で初期化されたfloat32型変数については、変数宣言で明示的に型が指定されなければなりません。

var foo float32 = 3.0

あるいは、foo := float32(3.0)のように定数を変換して型を指定する必要があります。

変数がヒープとスタックのどちらに割り当てられているかを知るには?

正しさの観点からは、知る必要はありません。Goの各変数は、それへの参照がある限り存在します。実装が選択した格納場所は、言語のセマンティクスとは無関係です。

格納場所は、効率的なプログラムを書く上で影響があります。可能な場合、Goコンパイラは関数にローカルな変数をその関数のスタックフレームに割り当てます。しかし、関数が戻った後にその変数が参照されないことをコンパイラが確証できない場合、ダングリングポインタのエラーを避けるために、コンパイラはその変数をガベージコレクションヒープ上に確保しなければなりません。また、ローカル変数が非常に大きい場合、スタックではなくヒープに格納する方が理にかなっている場合があります。

現在のコンパイラでは、変数のアドレスが取られている場合、その変数はヒープ上の割り当て候補となります。しかし、基本的なエスケープ解析では、そのような変数が関数からのリターンを越えて生きることはなく、スタックに存続できるケースもあると認識されています。

Goプロセスが仮想メモリを大量に使用しているのはなぜ?

Goのメモリアロケータは、仮想メモリの大きな領域をアロケーションの場所として予約します。この仮想メモリは特定のGoプロセスに局所的なもので、他のプロセスからメモリを奪うものではありません。

Goプロセスに割り当てられた実際のメモリ量を調べるには、Unixのtopコマンドを使用し、RES(Linux)またはRSIZE(macOS)の列を参照してください。

並行処理

アトミックな操作とは?ミューテックスは?

Goにおける操作の原子性についての説明は、Go Memory Modelのドキュメントにあります。

低レベルの同期とアトミックの基本要素はsyncおよびsync/atomicパッケージで利用可能です。これらのパッケージは、参照カウントのインクリメントや小規模な相互排他性の保証といった単純なタスクに適しています。並行するサーバ間の調整など、より高度な処理にはより高度なテクニックが必要であり、Goはゴルーチンやチャネルを通じてこのアプローチをサポートしています。たとえば、一度に一つのゴルーチンだけが特定のデータを処理するようにプログラムを構成することができます。このアプローチは、独自のGoの諺に要約されています。

メモリを共有することで通信してはいけない。代わりに、通信することによってメモリを共有する。

このコンセプトの詳細については、Share Memory By Communicatingのコードウォークとその関連記事を参照してください。

大規模な 並行プログラムは、これらのツールキットの両方を利用することが多いようです。

なぜCPUを増やしてもプログラムが速くならない?

CPUを増やしてプログラムが高速化するかどうかは、そのプログラムが解決しようとする問題に依存します。Go言語には、ゴルーチンやチャネルなどの並行処理用の基本要素がありますが、並行処理機構が並列処理を可能にするのは、問題が本質的に並列である場合だけです。本質的に逐次的な問題はCPUを増やしても高速化できませんが、並列実行可能な断片に分割できる問題は高速化でき、時には劇的に高速化されます。

CPUを増やすとプログラムが遅くなることがあります。実際問題として、有用な計算をするよりも同期や通信に多くの時間を費やしているプログラムは、OSのスレッドを複数使用することで性能が低下することがあります。これは、スレッド間のデータ受け渡しにはコンテキストの切り替えが必要で、これには大きなコストがかかり、そのコストはCPUの数が増えるほど大きくなるからです。例えば、Go仕様にあるエラトステネスのふるいの例では、多くのゴルーチンが起動されているものの目立った並列性はなく、スレッド(CPU)の数を増やすと速くなるよりも遅くなる可能性が高いです。

このテーマについての詳細は、並行処理は並列処理ではないと題した講演をご覧ください。

どうやってCPU数を制御する?

ゴルーチンが同時に実行できるCPUの数は、シェル環境変数GOMAXPROCSで制御されそのデフォルト値は利用可能なCPUコアの数です。そのため、並列実行の見込めるプログラムは、マルチCPUマシン上でデフォルトで並列実行を実現すべきです。使用する並列CPUの数を変更するには、環境変数を設定するか、runtimeパッケージの同名の関数を使用して、異なる数のスレッドを使用するようにランタイムサポートを設定します。1に設定すると、本当の意味での並列処理ができなくなり、独立したゴルーチンが交互に実行されるようになります。

ランタイムは、GOMAXPROCSの値より多くのスレッドを割り当てて、複数の未処理のI/Oリクエストに対応することができます。GOMAXPROCSは、実際に一度に実行できるゴルーチンの数にのみ影響し、 任意の数のゴルーチンがシステムコールでブロックされる可能性があります。

Goのゴルーチンスケジューラは、時間をかけて改善されてきましたが必要十分ではありません。将来は、OSのスレッドの使用をより最適化できるようになるかもしれません。今のところ、パフォーマンスに問題がある場合は、アプリケーションごとにGOMAXPROCSを設定することで解決できるかもしれません。

なぜゴルーチンにIDがない?

ゴルーチンは名前を持たない、単なる匿名ワーカーです。プログラマに一意な識別子も名前もデータ構造も公開しません。これに驚く人もいます。go文は、後でゴルーチンにアクセスしたり制御したりするために、何らかの項目を返すと期待してのことです。

ゴルーチンが匿名である根本的な理由は、並行実行されるコードをプログラミングするときにGo言語をフルに使えるようにするためです。対照的に、スレッドやゴルーチンに名前がついていると、その使用パターンがそれらを使用するライブラリでできることを制限してしまうことがあります。

ここでその難しさを説明します。一旦ゴルーチンに名前を付け、その周りにモデルを構築するとそれは特別なものになります。全ての計算をそのゴルーチンに関連付け、処理に複数の、場合によっては共有のゴルーチンを使用する可能性を無視することになってしまいます。もしnet/httpパッケージがリクエストごとの状態をゴルーチンに関連付けると、クライアントはリクエストを処理するときに、より多くのゴルーチンを使用することができなくなります。

さらに、グラフィックスシステム用のライブラリなど、すべての処理を「メインスレッド」で行う必要があるライブラリの経験から、このアプローチを並行処理言語で展開した場合、いかに厄介で制限の多いものになるかが分かっています。特殊なスレッドやゴルーチンの存在によってプログラマはプログラムをいびつな形にし、不用意に間違ったスレッドで操作してしまうことによるクラッシュなどの問題を避けなければならなくなるのです。

特定のゴルーチンが本当に特殊な場合、Goはチャンネルなどの機能を提供するため、それを柔軟に使ってやりとりすることができます。

関数とメソッド

なぜT*Tは異なるメソッドセットを持つ?

Goの仕様にあるように、あるT型のメソッド集合はレシーバがT型のすべてのメソッドからなり、対応するポインタ*T型のメソッド集合はレシーバが*TまたはTのすべてのメソッドからなります。つまり、*TのメソッドセットにはTのメソッドセットが含まれますが、その逆は成り立ちません。

この区別は、インターフェース値がポインタ*Tを含む場合メソッド呼び出しはそのポインタをデリファレンスすることで値を得ることができますが、インターフェース値が値Tを含む場合メソッド呼び出しがポインタを得るための安全な方法はないために生じるものです。(それができてしまうと、メソッドがインターフェース内の値の内容を変更することになるので言語仕様上許されません。)

コンパイラがメソッドに渡す値のアドレスを取ることができた場合でも、メソッドが値を変更するとその変更は呼び出し側で失われます。例として、bytes.BufferWriteメソッドがポインタではなく値のレシーバを使用する場合、このコードは標準入力をbuf自体ではなくbufコピーにコピーします。

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

これはほとんどの場合、望ましい挙動ではありません。

ゴルーチンとして実行されるクロージャーには何が起こる?

クロージャーを並行処理で使用する場合、若干の混乱が生じることがあります。次のようなプログラムを考えてみましょう。

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }
}

このとき、出力としてabcと表示されると勘違いしてしまうかもしれません。代わりに表示されるのは、cccです。これは、ループの各イテレーションが変数vの同じインスタンスを使用するため、各クロージャーがその単一の変数を共有するためです。クロージャーが実行されると、fmt.Printlnが実行された時点のvの値が表示されますが、vはゴルーチンが起動されてから変更された可能性があります。このような問題を事前に検出するために、go vetを実行します。

vの現在値を各クロージャの起動時にバインドするには、イテレーション毎に新しい変数を作成するように内部ループを変更する必要があります。一つの方法は、変数をクロージャーの引数として渡すことです。

    for _, v := range values {
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

この例では、vの値は無名関数の引数として渡されます。そして、その値は関数内で変数uとしてアクセスできます。

さらに簡単なのは、新しい変数を作成することです。奇妙に思えるかもしれませんが、Goではうまく機能する宣言スタイルを使っています。

    for _, v := range values {
        v := v // 新たしい「v」をつくる
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

繰り返しごとに新しい変数を定義しないという動作は、今にして思えば間違いだったのかもしれません。後のバージョンで対処されるかもしれませんが、互換性のためGoのバージョン1では変更することができません。

制御フロー

なぜGoには「?:」演算子がない?

Goには3項演算はありません。同じ結果を得るために、以下を使うことができます。

if expr {
    n = trueVal
} else {
    n = falseVal
}

Goに?:がないのは、この演算があまりに頻繁に使われて、理解しがたいほど複雑な式が作られるのをGoの設計者が見てきたからです。if-else形式の方が長いですが、間違いなく明確です。言語が必要とする条件付き制御フローの構文は1つだけです。

型パラメータ

なぜGoには型パラメータがある?

型パラメータは、いわゆるジェネリックプログラミングを可能にします。関数やデータ構造を使用する際に、その関数やデータ構造が後で指定される型によって定義されるというものです。たとえば、任意の順序型に属する2つの値の最小値を返す関数を書く場合、それぞれのとりうる型に対して別の関数を書く必要はありません。具体例を交えたより詳しい説明は、ブログ記事「Why Generics?」をご覧ください。

Goでジェネリックはどのように実装されている?

コンパイラは、それぞれのインスタンス化を個別にコンパイルするか、適度に類似したインスタンス化を単一の実装としてコンパイルするかを選択することができます。単一実装のアプローチは、インターフェースパラメータを持つ関数に似ています。コンパイラによって、異なるケースで異なる選択をすることになります。標準のGo 1.18コンパイラは、通常、同じ形状の型引数ごとに単一のインスタンス化を生成します。この場合、形状はサイズやそれが含むポインタの位置などの型のプロパティによって決定されます。将来のリリースでは、コンパイル時間、ランタイム効率、コードサイズの間のトレードオフを実験する予定です。

Goのジェネリクスと他の言語のジェネリクスを比較するとどうなる?

どの言語でも基本的な機能は同じで、後から指定される型を使って型や関数を書くことができます。そうは言うものの、いくつか異なる点もあります。

Java
Javaでは、コンパイラはコンパイル時にジェネリック型をチェックしますが、実行時にはその型を削除します。これは型消去として知られています。たとえば、コンパイル時にList<Integer>として知られていたJavaの型は、実行時には非ジェネリック型のListになります。これは、たとえばJava形式の型リフレクションを使うとき、List<Integer>型の値とList<Float>型の値とを区別することが不可能であることを意味します。Goでは、ジェネリック型のためのリフレクション情報は、コンパイル時の完全な型情報を含んでいます。
C++
伝統的にC++のテンプレートは型引数に制約をかけませんが、C++20ではコンセプトを介してオプションの制約がサポートされています。Goでは、制約はすべての型引数に対して必須です。C++20のコンセプトは、型引数とともにコンパイルする必要がある小さなコードフラグメントとして表現されます。Goの制約は、許可されたすべての型引数の集合を定義するインターフェース型です。
Rust
Rust版の制約をトレイト境界と呼びます。Rustでは、トレイト境界と型の関係は、トレイト境界を定義するクレートか型を定義するクレートで明示的に定義する必要があります。Goでは、Goの型が暗黙的にインターフェース型を実装するのと同じ様に、型引数が暗黙的に制約を満たします。Rust標準ライブラリは、比較や加算などの演算のための標準的なトレイトを定義しています。一方でGo標準ライブラリでは、これらはユーザーコードでインターフェース型を介して表現できるため、定義していません。
Python
Pythonは静的型付け言語ではないので、Pythonのすべての関数はデフォルトで常にジェネリックであると言えます。それらは常に任意の型の値で呼び出すことができ、型エラーは実行時に検出されます。

なぜGoは型パラメータリストに角括弧を使用する?

JavaとC++では、Java List<Integer>やC++ std::vector<int> のように、型パラメータリストに角括弧を使用します。しかし、構文上の問題が生じるためGoではその選択肢がとれません。v := F<T>のような関数内のコードを解析するとき、<を見た時点で、インスタンス化なのか<演算子を使った式なのかがあいまいになってしまうからです。これは、型情報がないと解決するのが非常に難しいのです。

例えば、次のような文を考えてみましょう。

a, b = w < x, y > (z)

型情報がなければ、代入の右辺が式のペア(w < xy > z)なのか、2つの結果値((w<x, y>)(z))を返すジェネリック関数のインスタンス化と呼び出しなのかを判断することは不可能です。

Goの設計上の重要な決定は、型情報なしで解析が可能であることですが、ジェネリクスに角括弧を使用する場合は不可能に思えます。

Goは角括弧を使うことに関してユニークでもオリジナルでもなく、Scalaのような他の言語でも角括弧をジェネリクスのコードに利用しています。

なぜGoは型パラメータを持つメソッドをサポートしない?

Goではジェネリック型にメソッドを持たせることができますが、レシーバ以外のメソッドの引数にはパラメータ化された型を使用することはできません。型のもつメソッドはその型が実装するインターフェースを決定しますが、ジェネリック型のメソッドのパラメータ化された引数でどのように動作するかは明らかではありません。この場合、実行時に関数をインスタンス化するか、すべての可能な型引数に対してすべてのジェネリック関数をインスタンス化する必要があります。どちらのアプローチも実現可能とは思えません。例を含む詳細については、提案を参照してください。型パラメータを持つメソッドの代わりに、型パラメータを持つトップレベル関数を使用するか、型パラメータをレシーバの型に追加します。

なぜパラメータ化された型のレシーバにより具体的な型を使えない?

ジェネリック型のメソッド宣言は、型の引数名を含むレシーバで記述されます。特定の型を使うことで、特定の型の引数に対してのみ動作するメソッドを生成することができると考える人もいるでしょう。

type S[T any] struct { f T }

func (s S[string]) Add(t string) string {
    return s.f + t
}

これは、operator + not defined on s.f (variable of type string constrained by any)というコンパイラエラーで失敗します。しかし、もちろん+演算子は事前に宣言されたstring型に対して動作します。

これは、Addメソッドの宣言でstringを使用しているのは単に型パラメータの名前を導入しているだけであり、その名前はstringであるためです。奇妙なことではありますが、妥当なことです。フィールドs.fが持つ型はstringですが、これは通常のあらかじめ宣言された型stringではなくSの型パラメータであり、このメソッドではstringという名前になっています。型パラメータの制約がanyなので、+演算子は許されません。

なぜコンパイラはプログラムの型引数を推論できない?

プログラマは、ジェネリック型や関数の型引数が何でなければならないかを容易に知ることができますが、Goがコンパイラにそれを推論することを許さない場合が多くあります。型推論は意図的に制限されており、どの型が推論されるかについて混乱が生じないように配慮されています。他の言語での経験から、予期せぬ型推論が行われると、プログラムを読んだりデバッグしたりする際にかなりの混乱が生じる可能性があることが分かっています。呼び出しの際に使用する明示的な型引数を指定することは、常に可能です。将来的には、ルールが単純かつ明確である限り、新しい推論形式がサポートされるかもしれません。

パッケージとテスト

どうやってマルチファイルのパッケージを作成する?

対象パッケージのソースファイルをすべて1つのディレクトリに配置します。ソースファイルは、異なるファイルの項目を自由に参照することができます。前方宣言やヘッダーファイルは必要ありません。

複数のファイルに分割されている以外は、単一ファイルのパッケージと同じようにコンパイルやテストを行います。

どうやってユニットテストを書く?

パッケージのソースと同じディレクトリに _test.goで終わる新しいファイルを作成します。そのファイルの中で、「testing」をインポートし、次のような形式の関数を記述します。

func TestFoo(t *testing.T) {
    ...
}

そのディレクトリでgo testを実行します。このスクリプトはTest関数を見つけ、テストバイナリをビルドし、実行します。

詳細はHow to Write Go Codeドキュメント、testingパッケージ、go testサブコマンドを参照してください。

お気に入りのテスト用ヘルパー関数はどこにある?

Goの標準テストパッケージではユニットテストを簡単に書くことができますが、 アサーション関数のような他の言語のテストフレームワークで提供されている機能がありません。このドキュメントの前のほうで、Goにアサーションがない理由を説明しましたが、 テストでassertを使用する場合にも同じことが当てはまります。適切なエラー処理とは、あるテストが失敗した後に他のテストを実行させ、 その失敗をデバッグする人が何が問題なのかを完全に把握できるようにすることです。isPrimeが2に対して間違った答えを返したので、 それ以降のテストを実行しない」 という報告よりも、「isPrimeが2、3、5、7(あるいは2、4、8、16)に対して間違った答えを返した」 という報告のほうが有用でしょう。テストの失敗を引き起こしたプログラマは、失敗したコードをよく知らないかもしれません。良いエラーメッセージを書くために費やした時間は、 後でテストが失敗したときに報われるでしょう。

関連するポイントとして、テストフレームワークは、条件分岐やコントロール、プリントメカニズムなど、独自のミニ言語に発展しがちですが、Goはすでにそれらの機能をすべて備えています。学ぶべき言語がひとつ減るし、そのアプローチによってテストが単純明快で理解しやすくなります。

良いエラーを書くために必要な余分なコードの量が反復的で圧倒されるようなら、テストはテーブル駆動で、データ構造(Goはデータ構造リテラルの優れたサポートを備えています)で定義された入力と出力のリストに対して反復する方がうまくいくかもしれません。良いテストと良いエラーメッセージを書くための労力は、多くのテストケースにわたって償却されます。標準のGoライブラリは、fmtパッケージのフォーマットテストのような、豊富な実例があります。

なぜXは標準ライブラリにない?

標準ライブラリの目的は、ランタイムをサポートし、オペレーティングシステムに接続し、フォーマットされたI/Oやネットワークなど、多くのGoプログラムが必要とする主要な機能を提供することです。また、暗号やHTTP、JSON、XMLなどの標準のサポートなど、Webプログラミングに重要な要素も含まれています。

長い間、これが唯一のGoライブラリであったため、何が含まれるかを定義する明確な基準はありません。しかし、現在は何が追加されるかを定義する基準はあります。

標準ライブラリに新しく追加されることはまれで、追加されるためのハードルは高いです。標準ライブラリに含まれるコードは、多大なメンテナンスコストがかかり(多くの場合、元の作者以外が負担)、Go 1互換性の約束に従うことになり、(APIの欠陥に対する修正が妨げられる)Goのリリーススケジュールに左右されるため、バグフィックスを迅速にユーザーに提供することができません。

ほとんどの新しいコードは標準ライブラリの外部に置かれ、go toolgo getコマンドでアクセスできるはずです。そのようなコードは、独自のメンテナ、リリースサイクル、および互換性の保証を持つことができます。ユーザーはpkg.go.devでパッケージを見つけたり、そのドキュメントを読んだりすることができます。

標準ライブラリには、log/syslogのような本来は属さないものもありますが、Go 1の互換性保証のため、私たちはライブラリ内の全コードを保守し続けています。しかし、ほとんどの新しいコードは別の場所に置くことをお勧めします。

実装

コンパイラの構築にはどのようなコンパイラ技術が使われている?

Goにはいくつかのプロダクションコンパイラがあり、またさまざまなプラットフォーム用に開発中のものもあります。

デフォルトのコンパイラであるgcは、Goディストリビューションに含まれており、goコマンドのサポートの一部となっています。gcはもともとC言語で書かれていましたが、これはブートストラップが困難だったためです。しかし、時代は進み、Go 1.5のリリース以来、コンパイラはGoプログラムになっています。この設計書講演で説明されているように、コンパイラは自動翻訳ツールを使ってCからGoに変換されました。こうしてコンパイラは「セルフホスティング」されるようになったのですが、これはつまり、ブートストラップの問題に向き合う必要があったということです。解決策は、C言語のインストールと同じように、Goのインストールがすでに動作している状態にしておくことです。新しいGo環境をソースから立ち上げる方法については、ここここに記載されています。

gcは再帰的降下パーサーを備えたGoで書かれており、同じくGoで書かれた、Plan 9ローダーをベースにしたカスタムローダーを使って、ELF/Mach-O/PEバイナリーを生成しています。

プロジェクト開始当初は、gcにLLVMを使うことを検討しましたが、私たちの性能目標を達成するには大きすぎ、遅すぎると判断しました。振り返ってみるとより重要なのは、LLVMで始めるとGoが必要とするスタック管理などのABIや関連する変更のうち、標準Cセットアップの一部ではないものを導入するのが難しくなってしまっていたでしょう。しかし、新しいLLVMの実装は、現在まとまり始めています。

gccgoコンパイラはC++で書かれたフロントエンドで、再帰的降下パーサが標準のGCCバックエンドに接続されています。

Goは、Goコンパイラを実装するのに適した言語であることが判明しましたが、これは本来の目的ではありませんでした。最初からセルフホスティングでなかったため、Goの設計は、ネットワークに接続されたサーバーという本来のユースケースに集中することができました。もし早い段階でGoは自分自身でコンパイルするべきだと決めていたら、コンパイラの構築をよりターゲットにした言語になっていたかもしれません。

gcはそれらを(まだ?)使っていませんが、ネイティブのレキサーとパーサーがgoパッケージで提供されていますし、ネイティブの型チェッカーも存在します。

ランタイムサポートはどのように実装されている?

ここでもブートストラップの問題があり、ランタイムコードはもともとほとんどCで書かれていましたが(ほんの少しのアセンブラを含む)、その後Goに翻訳されました(一部のアセンブラビットを除く)。gccgoのランタイムサポートはglibcを使用しています。gccgoコンパイラは、セグメント化スタックと呼ばれる技術を使ってゴルーチンを実装しており、最近のgoldリンカの修正でサポートされています。gollvmも同様に、対応するLLVMの基盤の上に構築されています。

なぜ些細なプログラムがこんなに大きなバイナリになる?

gcツールチェーンのリンカーは、デフォルトで静的にリンクされたバイナリを作成します。したがって、すべてのGoバイナリにはGoランタイムと動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要なランタイム型情報が含まれています。

Linuxでgccを使用して静的にコンパイル、リンクされた単純なCの「hello, world」プログラムは、printfの実装を含めて約750kB です。fmt.Printfを使った同等のGoプログラムは数メガバイトですが、これにはより強力なランタイムサポートと型情報およびデバッグ情報が含まれています。

gcでコンパイルしたGoプログラムを-ldflags=-wフラグでリンクすると、DWARFの生成が無効になり、バイナリからデバッグ情報が削除されますがそれ以外の機能は失われません。これにより、バイナリサイズを大幅に縮小することができます。

未使用の変数とインポートに関する文句を止めることはできる?

未使用の変数があるとバグがあるかもしれませんし、未使用のインポートはコンパイルを遅くするだけで、その影響はプログラムが時間とともにコードとプログラマーが増えるにつれて大きくなります。これらの理由から、Goは未使用の変数やインポートを含むプログラムのコンパイルを拒否し、短期的な利便性と長期的なビルド速度やプログラムの明瞭性を両立させています。

それでも、コードを開発するときには一時的にこのような状況を作り出すことはよくあることで、プログラムがコンパイルされる前にそれらを編集しなければならないのは煩わしいことかもしれません。

このようなチェックをオフにするか、少なくとも警告にとどめるコンパイラーオプションを求める声もあります。しかし、コンパイラオプションは言語のセマンティクスに影響を及ぼすべきではないですし、Goコンパイラは警告を報告せず、コンパイルを妨げるエラーのみを報告するためこのようなオプションは追加されていません。

警告を出さないのには2つの理由があります。第一に、もし文句を言う価値があるのなら、コードの中で修正する価値があります。(修正する価値がなければ、言及する価値もありません。)第二に、コンパイラが警告を生成することで、コンパイルをうるさくする弱いケースについて実装が警告を出すようになり、修正すべき本当のエラーを覆い隠してしまうからです。

とはいえ、この状況に対処するのは簡単です。空白の識別子を使い、開発中に未使用のものを存続させるのです。

import "unused"

// この宣言は、パッケージから項目を参照することで
// インポートが使用されることを示します。
var _ = unused.Item  // TODO: Delete before committing!

func main() {
    debugData := debug.Profile()
    _ = debugData // デバッグ時のみ使用します。
    ....
}

現在、ほとんどのGoプログラマはgoimportsというツールを使っています。これはGoソースファイルを自動的に書き換えて正しいインポートを持たせるもので、実際には使われていないインポートの問題を解消しています。このプログラムは、ほとんどのエディタに簡単に接続でき、Goのソースファイルが書かれたときに自動的に実行されます。

なぜウイルススキャンソフトウェアがGoディストリビューションやコンパイル済みバイナリが感染していると判断する?

特にWindowsマシンではよくあることで、ほとんどの場合誤検出です。市販のウイルススキャンプログラムは、他の言語でコンパイルされたバイナリほど頻繁に目にすることのないGoバイナリの構造にしばしば混乱するのです。

Goディストリビューションをインストールしただけでシステムが感染していると報告した場合、それは確かに間違いです。本当に徹底するには、チェックサムをダウンロードページにあるものと比較することでダウンロードを確認することができます。

いずれにせよ、レポートが誤りであると思われる場合は、お使いのウイルススキャナの供給元にバグを報告してください。そのうち、ウイルススキャナはGoプログラムを理解できるようになるかもしれません。

パフォーマンス

なぜGoはベンチマークXにおいてパフォーマンスが悪い?

Goの設計目標の1つは、同等のプログラムについてCの性能に近づけることですが、golang.org/x/exp/shootoutのいくつかのベンチマークを含め、Goはかなり悪い結果を出しています。最も遅いベンチマークは、同等の性能を持つバージョンがGoで利用できないライブラリに依存しています。たとえば、pidigits.goは多精度のmathパッケージに依存しており、CバージョンはGoと違ってGMP(最適化されたアセンブラで書かれています)を使用しています。正規表現に依存するベンチマーク(たとえばregex-dna.go)は、本質的にGoのネイティブregexpパッケージとPCREのような成熟し高度に最適化された正規表現ライブラリーを比較しています。

ベンチマークゲームは大規模なチューニングによって勝てるようになり、ほとんどのベンチマークのGoバージョンは注意が必要です。比較可能なCとGoのプログラムを測定すれば(reverse-complement.goはその一例です)、このスイートが示すよりも、2つの言語は純粋なパフォーマンスでずっと近いことがわかるでしょう。

それでも、改善の余地はあります。コンパイラの性能は良いですが、もっと良くなる可能性があります。多くのライブラリは性能面で大きな改善を必要としていますし、ガベージコレクタもまだ十分な速度ではありません。(たとえそうであっても、不要なガベージを生成しないように注意することは、大きな効果があります)。

いずれにせよ、Goは非常に高い競争力を発揮することがよくあります。言語とツールの発展に伴い、多くのプログラムのパフォーマンスは大幅に向上しています。参考になる例として、Goプログラムのプロファイリングに関するブログポストをご覧ください。

Cからの変更点

なぜC言語とこんなにも構文が異なる?

宣言のシンタックス以外には大きな違いはなく、2つの要望からきています。第一に、必須のキーワードや繰り返し、難解な表現があまりなく、軽快に感じられる構文にすることです。第二に、解析しやすいように設計されており、シンボルテーブルがなくても解析できることです。これにより、デバッガ、依存性解析器、自動文書抽出器、IDEプラグインなどのツールをより簡単に構築することができるようになりました。C言語とその子孫言語は、この点で難しいことで知られています。

なぜ宣言は後置?

Cに慣れていると後ろ向きな表現に見えます。Cでは、変数はその型を表す式のように宣言されるという考え方があります。これは良いアイデアですが、型と式の文法はあまりうまく混ざり合わないので、結果的に混乱することがあります。Goでは、式と型の文法はほとんど分離されており物事を単純化することができます(ポインタに接頭辞*を使うのは例外ですが、これはルールを証明しています)。

Cでは、つぎの宣言でaはポインタですがbは違います。

int* a, b;

Goでは、どちらもポインタです。

var a, b *int

この方がより明確で規則的です。また、:=という短い宣言形式は、完全な変数宣言は:=と同じ順序を示すべきだという主張をしているので、つぎの2つは同じ効果があります。

var a uint64 = 1
a := uint64(1)

また、式の文法だけでなく、型のための明確な文法を持つことで解析が簡単になります。funcchanなどのキーワードは物事を明確にします。

詳しくはGo's Declaration Syntaxという記事をご覧ください。

なぜポインタ演算がない?

安全性のためです。ポインタ演算がなければ、不正に成功したアドレスを絶対に導出できない言語を作ることができます。コンパイラやハードウェアの技術が発達し、配列のインデックスを使ったループでも、ポインタ演算を使ったループと同等の効率を実現できるようになりました。また、ポインタ演算がない分、ガベージコレクタの実装を簡略化することができます。

なぜ++--は文であって式ではない?またなぜ接頭辞でなく接尾辞?

ポインタ演算がない場合、前置および後置インクリメント演算子の利便性は低下します。式の階層からこれらを完全に取り除くことで、式の構文は単純化され、++--の評価順序に関する面倒な問題(f(i++)p[i] = q[++i] を考えてください)も取り除かれることになります。この簡略化には大きな意味があります。接尾辞と接頭辞に関しては、どちらでも良いのですが接尾辞の方がより伝統的です。接頭辞へのこだわりは、STLという、皮肉にも接尾辞のインクリメントを名前に含む言語のためのライブラリから生まれました。

なぜ中括弧はあってもセミコロンがない?またなぜ次の行に開始中括弧を置くことができない?

Goではステートメントのグループ化に中括弧を使いますが、これはC言語系の言語を使ったことがあるプログラマには馴染みのある構文です。しかし、セミコロンはパーサーのためのものであって、人間のためのものではないので、できるだけ排除したいと考えました。この目標を達成するために、GoはBCPLの技法を拝借しました。ステートメントを区切るセミコロンは正式な文法にありますが、レキサーがステートメントの終わりとなりうる行の最後に、先読みなしで自動的に挿入します。これは実際には非常にうまく機能するのですが、中括弧を使ったスタイルを強制してしまうという影響があります。たとえば、関数の開始中括弧は単独で1行に表示することはできません。

レキサーの中には、中括弧を次の行に生かすために先読みをするべきだと主張する人もいます。私たちはそうは思いません。Goのコードはgofmtによって自動的にフォーマットされることを意図しているので、何らかのスタイルを選択する必要があります。そのスタイルはCやJavaで使ってきたものと異なるかもしれませんが、Goは異なる言語であり、gofmtのスタイルは他のスタイルと同じくらい優れたものなのです。もっと重要なことは、すべてのGoプログラムに対してプログラム的に義務付けられた単一のフォーマットの利点は、特定のスタイルの欠点と思われるものを大きく上回るということです。Goのスタイルとは、Goのインタラクティブな実装が特別なルールなしに、一度に一行ずつ標準の構文を使えるということです。

なぜガベージコレクションをする?コストがかかりすぎるのでは?

システムプログラムの帳簿管理で最も大きなものの1つは、割り当てられたオブジェクトの寿命の管理です。C言語のように手作業で行っている言語では、プログラマの時間を大幅に消費し、悪質なバグの原因になることも少なくありません。また、C++やRustのように支援する仕組みが用意されている言語でも、その仕組みがソフトウェアの設計に大きな影響を与え、それ自体がプログラミングのオーバーヘッドになることも少なくありません。私たちは、このようなプログラマのオーバーヘッドをなくすことが重要であると考えました。そして、ここ数年のガベージコレクション技術の進歩により、十分安価に、そして十分低いレイテンシで実装できると確信し、ネットワークシステムにとって実行可能なアプローチとなりました。

並行プログラミングの難しさの多くは、オブジェクトの寿命問題に根ざしています。オブジェクトがスレッド間で受け渡されると、それらが安全に解放されることを保証するのが面倒になります。自動ガベージコレクションは、並行処理コードをはるかに書きやすくします。もちろん、ガベージコレクションを並行実行環境で実装すること自体が難題ですが、すべてのプログラムで実装するのではなく、一度だけ実装することですべての人の助けになります。

最後に、並行処理はさておき、ガベージコレクションはインターフェースをよりシンプルにします。なぜなら、インターフェース間でメモリがどのように管理されるかを指定する必要がなくなるからです。

Rustのような、リソースを管理する問題に新しいアイデアをもたらす言語での最近の研究が見当違いであると言っているわけではありません。私たちはこの取り組みを支持し、どのように発展していくのかに期待しています。しかし、Goはガベージコレクションだけを通じてオブジェクトの寿命に対処する、より伝統的なアプローチを取っています。

現在の実装は、マーク&スイープコレクタです。マシンがマルチプロセッサの場合、コレクタはメインプログラムと並行して別のCPUコアで実行されます。近年のコレクタに関する主要な取り組みによって、大きなヒープでも休止時間がミリ秒未満の範囲に収まることが多くなり、ネットワーク接続されたサーバーでのガベージコレクションに対する主要な反論の1つがほぼ解消されました。アルゴリズムを改良し、オーバーヘッドとレイテンシーをさらに削減し、新しいアプローチを模索する研究が続けられています。GoチームのRick Hudsonによる2018年のISMM基調講演では、これまでの進捗状況を説明し、今後のアプローチについて提案しています。

パフォーマンスの話題では、Goはガベージコレクション言語で一般的なものよりはるかに、プログラマにメモリのレイアウトと割り当て制御を委ねていることに留意してください。注意深いプログラマは、言語をうまく使うことでガベージコレクションのオーバーヘッドを劇的に減らすことができます。Goプログラムのプロファイリングについての記事で、Goのプロファイリングツールのデモを含む実用例をご覧ください。

About

Go公式のFAQを日本語訳したものです

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published