- Origins
- Usage
- Is Google using Go internally?
- What other companies use Go?
- Do Go programs link with C/C++ programs?
- なぜ型 T は Equal インターフェースを満たさないのか?
- []T を []interface{} に変換できますか?
- T1 と T2 が同じ基底型を持つ場合、[]T1 を []T2 に変換できますか?
- なぜ私の nil エラー値は nil と等しくないのか?
- なぜゼロサイズ型は奇妙に振る舞うのか?
- なぜ C のような未タグ付きユニオンがないのか?
- なぜ Go にはバリアント型がないのか?
- なぜ Go には共変結果型がないのか?
- 値
- コードを書く
- ポインタと割り当て
- 並行性
- 関数とメソッド
- 制御フロー
- 型パラメータ
- パッケージとテスト
- 実装
- パフォーマンス
- Cからの変更点
Origins
What is the purpose of the project?
2007年にGoが誕生したとき、プログラミングの世界は今日とは異なっていました。生産用ソフトウェアは通常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の記事で入手できます。
What is the history of the project?
ロバート・グリエセマー、ロブ・パイク、ケン・トンプソンは、2007年9月21日にホワイトボードで新しい言語の目標をスケッチし始めました。数日以内に、目標は何かをするための計画に落ち着き、それが何であるかの大まかなアイデアが固まりました。設計は、無関係な作業と並行してパートタイムで続けられました。2008年1月までに、ケンはアイデアを探求するためのコンパイラの作業を始めました。それはCコードを出力として生成しました。年の中頃までに、言語はフルタイムのプロジェクトとなり、製品コンパイラを試みるのに十分に固まりました。2008年5月、イアン・テイラーは、ドラフト仕様を使用してGoのGCCフロントエンドに独立して取り組み始めました。ラッス・コックスは2008年末に参加し、言語とライブラリをプロトタイプから現実に移行するのを助けました。
Goは2009年11月10日に公開オープンソースプロジェクトとなりました。コミュニティから無数の人々がアイデア、議論、コードを提供しています。
現在、世界中に数百万のGoプログラマー(ゴファー)が存在し、毎日増えています。Goの成功は私たちの期待をはるかに超えています。
What’s the origin of the gopher mascot?
マスコットとロゴは、ルネ・フレンチによってデザインされ、彼女はグレンダ、プラン9のバニーもデザインしました。ゴファーに関するブログ記事では、彼女が数年前にWFMUのTシャツデザインに使用したものから派生したことが説明されています。ロゴとマスコットはクリエイティブ・コモンズ 表示 4.0ライセンスの下で保護されています。
ゴファーには、彼の特徴とそれを正しく表現する方法を示すモデルシートがあります。モデルシートは、2016年のGopherconでルネが行った講演で初めて示されました。彼には独自の特徴があります。彼はGoゴファーであり、ただの古いゴファーではありません。
Is the language called Go or Golang?
言語はGoと呼ばれています。「golang」という名称は、ウェブサイトが元々golang.orgであったために生じました。(当時は.devドメインはありませんでした。)多くの人がgolangという名前を使用していますが、それはラベルとして便利です。たとえば、言語のソーシャルメディアタグは「#golang」です。言語の名前は単にGoです。
余談ですが、公式ロゴには2つの大文字がありますが、言語名はGoと書かれています。
Why did you create a new language?
Goは、私たちがGoogleで行っていた作業に対する既存の言語と環境へのフラストレーションから生まれました。プログラミングはあまりにも難しくなり、言語の選択が一因でした。効率的なコンパイル、効率的な実行、またはプログラミングの容易さのいずれかを選択しなければならず、すべて三つを同時に提供する主流の言語は存在しませんでした。できるプログラマーは、C++や、やや少ない程度のJavaではなく、PythonやJavaScriptのような動的型付け言語に移行することで、安全性と効率性よりも容易さを選んでいました。
私たちはこの懸念を抱いているのは私たちだけではありませんでした。プログラミング言語の風景がかなり静かだった何年もの後、GoはRust、Elixir、Swiftなどのいくつかの新しい言語の中で最初のものの一つであり、プログラミング言語の開発を再び活発でほぼ主流の分野にしました。
Goは、解釈された動的型付け言語のプログラミングの容易さと、静的型付けコンパイル言語の効率性と安全性を組み合わせることを試みることで、これらの問題に対処しました。また、ネットワーク化されたマルチコアコンピューティングをサポートし、現在のハードウェアにより適応することを目指しました。最後に、Goでの作業は速いことを意図しています:単一のコンピュータ上で大きな実行可能ファイルをビルドするのに数秒以上かかるべきではありません。これらの目標を達成することは、現在の言語からのプログラミングアプローチを再考することにつながり、合成的な型システム、同時実行性とガーベジコレクションのサポート、依存関係の厳格な仕様などを導きました。これらはライブラリやツールではうまく処理できないため、新しい言語が必要とされました。
Go at Googleの記事では、Go言語の設計の背景と動機について議論し、このFAQで提示された多くの回答についての詳細を提供しています。
What are Go’s ancestors?
Goは主にCファミリー(基本構文)に属し、Pascal/Modula/Oberonファミリー(宣言、パッケージ)からの重要な入力があり、Tony HoareのCSPに触発された言語(NewsqueakやLimboなど)からのいくつかのアイデアも含まれています(同時実行性)。しかし、全体としては新しい言語です。あらゆる点において、言語はプログラマーが何をするか、そして私たちが行うプログラミングをより効果的に、つまりより楽しくするために考えながら設計されました。
What are the guiding principles in the design?
Goが設計されたとき、JavaとC++は少なくともGoogleでサーバーを書くために最も一般的に使用されている言語でした。私たちは、これらの言語が過度の帳簿管理と繰り返しを必要とすると感じました。一部のプログラマーは、効率性と型安全性を犠牲にして、Pythonのようなより動的で流動的な言語に移行しました。私たちは、単一の言語で効率性、安全性、流動性を持つことが可能であるべきだと感じました。
Goは、両方の意味でのタイピングの量を減らすことを試みています。設計全体を通じて、私たちは混乱と複雑さを減らすことを試みました。前方宣言やヘッダーファイルはなく、すべては正確に一度だけ宣言されます。初期化は表現力豊かで自動的で使いやすいです。構文はクリーンでキーワードが少ないです。繰り返し(foo.Foo* myFoo = new(foo.Foo)
)は、:=
宣言と初期化構文を使用した単純な型導出によって減少します。そして、おそらく最も急進的な点は、型階層がないことです:型は単に存在し、その関係を発表する必要はありません。これらの簡素化により、Goは表現力豊かでありながら、理解可能で、生産性を犠牲にすることなく実現できます。
もう一つの重要な原則は、概念を直交させることです。メソッドは任意の型に対して実装できます。構造体はデータを表し、インターフェースは抽象を表します。直交性は、物事が組み合わさったときに何が起こるかを理解しやすくします。
Usage
Is Google using Go internally?
はい。GoはGoogle内部で広く生産に使用されています。例として、Googleのダウンロードサーバーであるdl.google.com
は、Chromeのバイナリやapt-get
パッケージなどの大きなインストール可能ファイルを配信しています。
GoはGoogleで使用されている唯一の言語ではなく、遠く及びませんが、サイト信頼性エンジニアリング(SRE)や大規模データ処理などのいくつかの分野にとって重要な言語です。また、Google Cloudを運営するソフトウェアの重要な部分でもあります。
What other companies use Go?
Goの使用は世界中で増加しており、特にクラウドコンピューティング分野で顕著ですが、これに限りません。Goで書かれた主要なクラウドインフラプロジェクトにはDockerやKubernetesがありますが、他にも多くあります。
クラウドだけでなく、go.devウェブサイトにある企業のリストやいくつかの成功事例からもわかるように、さまざまな分野で使用されています。また、Go Wikiには、Goを使用している多くの企業をリストしたページが定期的に更新されています。
Wikiには、言語を使用している企業やプロジェクトに関するさらなる成功事例へのリンクが含まれています。
Do Go programs link with C/C++ programs?
CとGoを同じアドレス空間で一緒に使用することは可能ですが、自然な適合ではなく、特別なインターフェースソフトウェアが必要になることがあります。また、CとGoコードをリンクすると、Goが提供するメモリ安全性やスタック管理の特性を失います。問題を解決するためにCライブラリを使用することが絶対に必要な場合もありますが、その場合は常に純粋なGoコードには存在しないリスクが伴うため、注意して行う必要があります。
CをGoと一緒に使用する必要がある場合、進め方はGoコンパイラの実装によって異なります。「標準」コンパイラは、GoogleのGoチームによってサポートされているGoツールチェーンの一部で、gc
と呼ばれています。さらに、GCCベースのコンパイラ(gccgo
)やLLVMベースのコンパイラ(gollvm
)、さまざまな目的に応じた異常なコンパイラのリストも増えています。
また、`````cgo`````とSWIGを`````gccgo`````および`````gollvm`````と一緒に使用することもできます。これらは従来のABIを使用しているため、注意深く行えば、これらのコンパイラからのコードをGCC/LLVMでコンパイルされたCまたはC++プログラムと直接リンクすることも可能です。ただし、安全に行うには、関係するすべての言語の呼び出し規約を理解し、GoからCまたはC++を呼び出す際のスタック制限に注意する必要があります。
<a name="ide"></a>
### What IDEs does Go support?
GoプロジェクトにはカスタムIDEは含まれていませんが、言語とライブラリはソースコードを分析しやすくするように設計されています。その結果、ほとんどの有名なエディタやIDEは、直接またはプラグインを通じてGoをよくサポートしています。
Goチームは、LSPプロトコル用のGo言語サーバーである[`````gopls`````](https://pkg.go.dev/golang.org/x/tools/gopls#section-readme)もサポートしています。LSPをサポートするツールは、`````gopls`````を使用して言語固有のサポートを統合できます。
Goを良好にサポートする有名なIDEやエディタのリストには、Emacs、Vim、VSCode、Atom、Eclipse、Sublime、IntelliJ(GoLandというカスタムバリアントを通じて)などが含まれます。あなたのお気に入りの環境は、Goでのプログラミングに生産的なものである可能性が高いです。
<a name="protocol_buffers"></a>
### Does Go support Google’s protocol buffers?
必要なコンパイラプラグインとライブラリを提供する別のオープンソースプロジェクトがあります。これは[github.com/golang/protobuf/](https://github.com/golang/protobuf)で入手できます。
<a name="Design"></a>
## Design
<a name="runtime"></a>
### Does Go have a runtime?
Goには、*runtime*と呼ばれる広範なランタイムライブラリがあり、これはすべてのGoプログラムの一部です。このライブラリは、ガーベジコレクション、同時実行性、スタック管理、Go言語の他の重要な機能を実装しています。Goのランタイムは言語にとってより中心的なものであり、Cライブラリである`````libc`````に類似しています。
ただし、Goのランタイムには、Javaランタイムが提供するような仮想マシンは含まれていないことを理解することが重要です。Goプログラムは、ネイティブマシンコード(またはJavaScriptやWebAssembly、一部のバリアント実装の場合)に事前にコンパイルされます。したがって、プログラムが実行される仮想環境を説明するためにこの用語がよく使用されますが、Goでは「ランタイム」という言葉は、重要な言語サービスを提供するライブラリに付けられた名前に過ぎません。
<a name="unicode_identifiers"></a>
### What’s up with Unicode identifiers?
Goを設計する際、私たちはそれが過度にASCII中心でないことを確認したいと考え、識別子の空間を7ビットASCIIの制約から拡張しました。Goのルール—識別子の文字はUnicodeで定義された文字または数字でなければならない—は理解しやすく、実装も簡単ですが、制約があります。結合文字は設計上除外されており、これによりデーヴァナーガリーなどのいくつかの言語が除外されます。
このルールにはもう一つの不幸な結果があります。エクスポートされた識別子は大文字の文字で始まる必要があるため、いくつかの言語の文字から作成された識別子は、定義上エクスポートできません。今のところ、唯一の解決策は`````X日本語`````のようなものを使用することですが、これは明らかに満足のいくものではありません。
言語の最初のバージョン以来、他の母国語を使用するプログラマーに対応するために識別子空間を拡張する最良の方法についてかなりの考慮がなされてきました。具体的に何をするかは現在も活発な議論のトピックであり、将来のバージョンの言語では識別子の定義がより自由になる可能性があります。たとえば、Unicode組織の[推奨事項](http://unicode.org/reports/tr31/)からのいくつかのアイデアを採用するかもしれません。何が起こるにせよ、識別子の可視性を決定する方法を保持(または拡張)しながら、互換性を持って行う必要があります。これはGoの最も好きな機能の一つです。
当面の間、プログラムを壊すことなく後で拡張できるシンプルなルールを持っています。このルールは、あいまいな識別子を許可するルールから生じるバグを回避します。
<a name="Why_doesnt_Go_have_feature_X"></a>
### Why does Go not have feature X?
すべての言語には新しい機能が含まれ、誰かのお気に入りの機能が省かれています。Goは、プログラミングの快適さ、コンパイルの速さ、概念の直交性、同時実行性やガーベジコレクションなどの機能をサポートする必要性を考慮して設計されました。あなたのお気に入りの機能が欠けているのは、それが合わないからか、コンパイル速度や設計の明確さに影響を与えるからか、または基本的なシステムモデルを難しくするからかもしれません。
Goに機能*X*が欠けていることが気になる場合は、私たちを許してください。そして、Goが持っている機能を調査してください。あなたはそれらが*X*の欠如を興味深い方法で補うことを見つけるかもしれません。
<a name="generics"></a>
### When did Go get generic types?
Go 1.18リリースでは、言語に型パラメータが追加されました。これにより、ポリモーフィックまたはジェネリックプログラミングの形式が可能になります。詳細については、[言語仕様](/read/go-latest/bd36d26997de6f01.md)および[提案](https://golang.org/design/43651-type-parameters)を参照してください。
<a name="beginning_generics"></a>
### Why was Go initially released without generic types?
Goは、時間の経過とともにメンテナンスが容易なサーバープログラムを書くための言語として意図されていました。(詳細については[この記事](https://golang.org/talks/2012/splash.article)を参照してください。)設計は、スケーラビリティ、可読性、同時実行性などに集中していました。ポリモーフィックプログラミングは当時の言語の目標にとって必須とは思えず、したがって初めは簡素化のために省かれました。
ジェネリックは便利ですが、型システムとランタイムの複雑さを伴います。私たちは、複雑さに見合った価値を提供する設計を開発するのに時間がかかりました。
<a name="exceptions"></a>
### Why does Go not have exceptions?
私たちは、例外を制御構造に結びつけること(`````try-catch-finally`````のイディオムのように)が、複雑なコードを生むと考えています。また、プログラマーがファイルを開けないなどの通常のエラーを例外としてラベル付けすることを奨励する傾向があります。
Goは異なるアプローチを取ります。通常のエラーハンドリングに関しては、Goのマルチバリュー戻り値により、戻り値を過負荷にすることなくエラーを報告することが容易です。[Goの他の機能と組み合わせた標準的なエラー型](https://golang.org/doc/articles/error_handling.html)により、エラーハンドリングは快適ですが、他の言語とはかなり異なります。
Goには、真に例外的な条件を示すためのいくつかの組み込み関数もあります。リカバリメカニズムは、エラー後に関数の状態が破棄される際にのみ実行され、これは災害を処理するのに十分ですが、追加の制御構造を必要とせず、うまく使用すればクリーンなエラーハンドリングコードを生むことができます。
詳細については、[Defer, Panic, and Recover](https://golang.org/doc/articles/defer_panic_recover.html)の記事を参照してください。また、[Errors are values](https://golang.org/blog/errors-are-values)のブログ記事では、エラーが単なる値であるため、Goの完全な力をエラーハンドリングに展開できることを示すことによって、Goでエラーをクリーンに処理する一つのアプローチを説明しています。
<a name="assertions"></a>
### Why does Go not have assertions?
Goはアサーションを提供していません。アサーションは間違いなく便利ですが、私たちの経験では、プログラマーはそれを使って適切なエラーハンドリングや報告を考えることを避けるための足場として使用します。適切なエラーハンドリングは、サーバーが致命的でないエラーの後も動作し続けることを意味します。適切なエラーレポートは、エラーが直接的で要点を押さえたものであり、プログラマーが大きなクラッシュトレースを解釈するのを助けます。正確なエラーは、エラーを見ているプログラマーがコードに不慣れな場合に特に重要です。
私たちは、これは論争のポイントであることを理解しています。Go言語やライブラリには、現代の慣行とは異なる多くのことがありますが、それは時には異なるアプローチを試みる価値があると感じているからです。
<a name="csp"></a>
### Why build concurrency on the ideas of CSP?
同時実行性とマルチスレッドプログラミングは、時間の経過とともに難しさの評判を得てきました。私たちは、これは[pthread](https://en.wikipedia.org/wiki/POSIX_Threads)のような複雑な設計と、ミューテックス、条件変数、メモリバリアなどの低レベルの詳細に過度に重点を置くことが一因であると考えています。高レベルのインターフェースは、たとえミューテックスなどが内部で存在しても、はるかにシンプルなコードを可能にします。
高レベルの言語サポートを提供するための最も成功したモデルの一つは、ホアのコミュニケーティング・シーケンシャル・プロセス(CSP)から来ています。オッカムやエラーニングは、CSPから派生したよく知られた言語の二つです。Goの同時実行性プリミティブは、ファミリーツリーの異なる部分から派生しており、その主な貢献は、チャンネルを第一級オブジェクトとして扱うという強力な概念です。いくつかの以前の言語での経験は、CSPモデルが手続き型言語のフレームワークにうまく適合することを示しています。
<a name="goroutines"></a>
### Why goroutines instead of threads?
ゴルーチンは、同時実行性を使いやすくするための一部です。このアイデアはしばらく前から存在しており、独立して実行される関数—コルーチン—をスレッドのセットに多重化することです。コルーチンがブロックされると、たとえばブロッキングシステムコールを呼び出すことによって、ランタイムは自動的に同じオペレーティングシステムスレッド上の他のコルーチンを別の実行可能なスレッドに移動させ、ブロックされないようにします。プログラマーはこれを全く見ることはなく、これがポイントです。その結果、私たちはゴルーチンと呼ぶものは非常に安価です:スタックのメモリを除いて、オーバーヘッドはわずか数キロバイトです。
スタックを小さくするために、Goのランタイムはリサイズ可能で制限されたスタックを使用します。新しく作成されたゴルーチンには数キロバイトが与えられ、これはほとんど常に十分です。十分でない場合、ランタイムはスタックを保存するためのメモリを自動的に増やし(および減らし)、多くのゴルーチンが適度な量のメモリで生き続けることを可能にします。CPUオーバーヘッドは、関数呼び出しごとに平均約3つの安価な命令です。同じアドレス空間で数十万のゴルーチンを作成することは実用的です。もしゴルーチンが単なるスレッドであったなら、システムリソースははるかに少ない数で枯渇してしまったでしょう。
<a name="atomic_maps"></a>
### Why are map operations not defined to be atomic?
長い議論の末、マップの典型的な使用は複数のゴルーチンからの安全なアクセスを必要としないと決定されました。そして、必要な場合、マップはすでに同期されている大きなデータ構造や計算の一部である可能性が高いです。したがって、すべてのマップ操作がミューテックスを取得することを要求すると、ほとんどのプログラムが遅くなり、少数のプログラムにしか安全性を追加しませんでした。しかし、これは簡単な決定ではありませんでした。なぜなら、制御されていないマップアクセスがプログラムをクラッシュさせる可能性があるからです。
言語は原子的なマップ更新を禁止していません。必要な場合、たとえば信頼できないプログラムをホストする場合、実装はマップアクセスを相互ロックすることができます。
マップアクセスは、更新が行われているときのみ安全ではありません。すべてのゴルーチンが単に読み取り(マップ内の要素を検索し、`````for````` `````range`````ループを使用してそれを反復することを含む)を行い、要素に割り当てたり削除したりすることによってマップを変更しない限り、同期なしで同時にマップにアクセスすることは安全です。
マップの正しい使用を助けるために、言語のいくつかの実装には、並行実行によってマップが不適切に変更されたときにランタイムで自動的に報告する特別なチェックが含まれています。また、syncライブラリには、特定の使用パターン(静的キャッシュなど)にうまく機能する[`````sync.Map`````](https://pkg.go.dev/sync#Map)という型がありますが、これは組み込みのマップ型の一般的な置き換えとしては適していません。
<a name="language_changes"></a>
### Will you accept my language change?
人々はしばしば言語の改善を提案しますが—[メーリングリスト](https://groups.google.com/group/golang-nuts)にはそのような議論の豊かな歴史があります—これらの変更が受け入れられることは非常に少ないです。
Goはオープンソースプロジェクトですが、言語とライブラリは[互換性の約束](https://golang.org/doc/go1compat.html)によって保護されており、既存のプログラムを壊す変更を防いでいます。少なくともソースコードレベルでは(プログラムは時折最新の状態を保つために再コンパイルする必要があるかもしれません)。あなたの提案がGo 1仕様に違反している場合、その価値に関わらず、私たちはそのアイデアを考慮することすらできません。将来のGoの主要なリリースはGo 1と互換性がないかもしれませんが、そのトピックに関する議論は始まったばかりであり、確かなことは、プロセスで導入される互換性のない変更は非常に少ないということです。さらに、互換性の約束は、古いプログラムがその状況に適応するための自動的な道筋を提供することを奨励します。
たとえあなたの提案がGo 1仕様と互換性があったとしても、それはGoの設計目標の精神に反するかもしれません。*[Go at Google: Language Design in the Service of Software Engineering](https://golang.org/talks/2012/splash.article)*の記事は、Goの起源とその設計の背後にある動機を説明しています。
<a name="types"></a>
## Types
<a name="Is_Go_an_object-oriented_language"></a>
### Is Go an object-oriented language?
はい、そしていいえ。Goには型とメソッドがあり、オブジェクト指向スタイルのプログラミングを許可しますが、型階層はありません。Goの「インターフェース」の概念は、使いやすく、ある意味でより一般的なアプローチを提供します。また、他の型に型を埋め込む方法もあり、サブクラス化に類似したものを提供します。さらに、GoのメソッドはC++やJavaよりも一般的です:それらは任意の種類のデータ、平凡な「アンボックス」整数などの組み込み型に対しても定義できます。構造体(クラス)に制限されていません。
また、型階層がないため、Goの「オブジェクト」はC++やJavaのような言語よりもはるかに軽量に感じられます。
<a name="How_do_I_get_dynamic_dispatch_of_methods"></a>
### How do I get dynamic dispatch of methods?
動的にディスパッチされたメソッドを持つ唯一の方法はインターフェースを通じてです。構造体や他の具体的な型のメソッドは常に静的に解決されます。
<a name="inheritance"></a>
### Why is there no type inheritance?
オブジェクト指向プログラミングは、少なくとも最もよく知られた言語では、型間の関係についての議論が多すぎます。これらの関係はしばしば自動的に導出できるものです。Goは異なるアプローチを取ります。
プログラマーが事前に二つの型が関連していると宣言する必要があるのではなく、Goでは型はそのメソッドのサブセットを指定するインターフェースを自動的に満たします。帳簿管理を減らすだけでなく、このアプローチには実際の利点があります。型は一度に多くのインターフェースを満たすことができ、従来の多重継承の複雑さなしに。インターフェースは非常に軽量であり、1つまたはゼロのメソッドを持つインターフェースは有用な概念を表現できます。新しいアイデアが登場した場合やテストのために、元の型を注釈なしで後からインターフェースを追加できます。型とインターフェースの間に明示的な関係がないため、管理や議論の必要がある型階層はありません。
これらのアイデアを使用して、型安全なUnixパイプに類似したものを構築することが可能です。たとえば、`````fmt.Fprintf`````がファイルだけでなく任意の出力へのフォーマットされた印刷を可能にする方法や、`````bufio`````パッケージがファイルI/Oとは完全に独立している方法、`````image`````パッケージが圧縮画像ファイルを生成する方法などです。これらのアイデアは、単一のメソッド(`````Write`````)を表す単一のインターフェース(`````io.Writer`````)から派生しています。そして、これはほんの表面をなぞるに過ぎません。Goのインターフェースは、プログラムの構造に深い影響を与えています。
慣れるのには少し時間がかかりますが、この暗黙の型依存スタイルはGoの最も生産的な点の一つです。
<a name="methods_on_basics"></a>
### Why is len a function and not a method?
この問題について議論しましたが、`````len`````やその仲間を関数として実装することは実際には問題ないと決定し、基本型のインターフェース(Go型の意味で)に関する問題を複雑にしませんでした。
<a name="overloading"></a>
### Why does Go not support overloading of methods and operators?
メソッドのディスパッチは、型マッチングを必要としない場合に簡素化されます。他の言語での経験から、同じ名前のさまざまなメソッドが異なるシグネチャを持つことは時折便利ですが、実際には混乱を招き、脆弱になる可能性があることがわかりました。名前だけで一致させ、型の一貫性を要求することは、Goの型システムにおける主要な簡素化の決定でした。
演算子のオーバーロードに関しては、それは絶対的な要件というよりは便利さのようです。再び、これがない方が物事は簡単です。
<a name="implements_interface"></a>
### Why doesn’t Go have “implements” declarations?
Goの型は、そのインターフェースのメソッドを実装することによってインターフェースを実装します。それ以上のものはありません。この特性により、既存のコードを変更することなくインターフェースを定義して使用することができます。これは、関心の分離を促進し、コードの再利用を改善し、コードが発展するにつれて現れるパターンに基づいて構築するのを容易にする[構造的型付け](https://en.wikipedia.org/wiki/Structural_type_system)の一種を可能にします。インターフェースの意味は、Goの軽快で軽量な感覚の主な理由の一つです。
詳細については、[型継承に関する質問](#inheritance)を参照してください。
<a name="guarantee_satisfies_interface"></a>
### How can I guarantee my type satisfies an interface?
型`````T`````がインターフェース`````I`````を実装していることをコンパイラに確認させるには、`````T`````のゼロ値または`````T`````へのポインタを使用して代入を試みます。
``````bash
type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.
`
もしT
(または*T
、それに応じて)がI
を実装していなければ、その間違いはコンパイル時に捕捉されます。
インターフェースのユーザーに明示的にそれを実装することを宣言させたい場合は、インターフェースのメソッドセットに説明的な名前のメソッドを追加できます。たとえば:
type Fooer interface {
Foo()
ImplementsFooer()
}
型はImplementsFooer
メソッドを実装する必要があり、Fooer
であることを明確に文書化し、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
いくつかの多相型システムにおける類似の状況とは異なり、T
は Equaler
を実装していません。T.Equal
の引数の型は T
であり、文字通り必要な型 Equaler
ではありません。
Go では、型システムは Equal
の引数を昇格させません。これはプログラマーの責任であり、T2
型が Equaler
を実装していることからも示されています:
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 では、T3
は Opener
を満たしませんが、他の言語では満たすかもしれません。
Go の型システムがそのような場合にプログラマーのために少ないことは事実ですが、サブタイピングがないため、インターフェースの満足に関するルールは非常に簡単に述べることができます:関数の名前とシグネチャはインターフェースのものと正確に一致していますか? Go のルールは効率的に実装するのも簡単です。私たちは、これらの利点が自動型昇格の欠如を補うと感じています。
[]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 と等しくないのか?
内部的には、インターフェースは2つの要素、型 T
と値 V
として実装されています。V
は int
、struct
またはポインタのような具体的な値であり、決してインターフェース自体ではなく、T
型を持ちます。たとえば、int
値 3 をインターフェースに格納すると、結果として得られるインターフェース値は、概念的には、(T=int
, V=3
) となります。値 V
はインターフェースの 動的 値としても知られており、特定のインターフェース変数はプログラムの実行中に異なる値 V
(および対応する型 T
)を保持する可能性があります。
インターフェース値は nil
の場合にのみ、V
と T
の両方が未設定である場合(T=nil
、V
は設定されていません)。特に、nil
インターフェースは常に nil
型を保持します。nil
型のポインタをインターフェース値内に格納すると、内部型はポインタの値に関係なく *int
になります:(T=*int
, V=nil
)。したがって、そのようなインターフェース値は非 nil
であり、内部のポインタ値 V
が nil
であってもです。
この状況は混乱を招く可能性があり、nil
値が error
戻り値のようなインターフェース値内に格納されると発生します:
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
すべてがうまくいけば、関数は nil
p
を返し、戻り値は (error
インターフェース値を保持します。これは、呼び出し元が返されたエラーを nil
と比較すると、何も悪いことが起こらなかった場合でも常にエラーがあったかのように見えることを意味します。呼び出し元に適切な nil
error
を返すには、関数は明示的な nil
を返す必要があります:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
エラーを返す関数は、常にそのシグネチャで error
型を使用することが良いアイデアです(上記のように)、*MyError
のような具体的な型ではなく、エラーが正しく作成されることを保証するためです。たとえば、os.Open
は error
を返しますが、nil
でない場合でも、常に具体的な型 *os.PathError
です。
ここで説明したような類似の状況は、インターフェースが使用されるたびに発生する可能性があります。インターフェースに具体的な値が格納されている場合、インターフェースは nil
ではないことを覚えておいてください。詳細については、反射の法則を参照してください。
なぜゼロサイズ型は奇妙に振る舞うのか?
Go は、フィールドを持たない構造体(struct{}
)や要素を持たない配列([0]byte
)などのゼロサイズ型をサポートしています。ゼロサイズ型には何も格納できませんが、これらの型は値が必要ない場合、map[int]struct{}
のように便利です。また、メソッドはあるが値がない型にも役立ちます。
ゼロサイズ型の異なる変数は、メモリ内の同じ位置に配置されることがあります。これは、これらの変数に値を格納できないため、安全です。
さらに、言語は、2つの異なるゼロサイズ変数へのポインタが等しいかどうかについての保証を行いません。そのような比較は、プログラムのある時点で true
を返し、別の時点で false
を返すことさえあります。これは、プログラムがどのようにコンパイルされ、実行されるかによって異なります。
ゼロサイズ型に関する別の問題は、ゼロサイズ構造体フィールドへのポインタがメモリ内の別のオブジェクトへのポインタと重ならないようにする必要があることです。これにより、ガベージコレクタで混乱が生じる可能性があります。これは、構造体の最後のフィールドがゼロサイズの場合、最後のフィールドへのポインタが構造体の直後のメモリと重ならないように構造体がパディングされることを意味します。したがって、このプログラム:
func main() {
type S struct {
f1 byte
f2 struct{}
}
fmt.Println(unsafe.Sizeof(S{}))
}
は、ほとんどの Go 実装で 2
を印刷し、1
ではありません。
なぜ C のような未タグ付きユニオンがないのか?
未タグ付きユニオンは、Go のメモリ安全性の保証に違反します。
なぜ Go にはバリアント型がないのか?
バリアント型(代数型とも呼ばれる)は、値が他の型のセットのいずれかを取る可能性があることを指定する方法を提供しますが、それらの型のみです。システムプログラミングの一般的な例は、エラーがネットワークエラー、セキュリティエラー、またはアプリケーションエラーのいずれかであることを指定し、呼び出し元がエラーの型を調べることで問題の発生源を識別できるようにします。別の例は、各ノードが異なる型(宣言、文、代入など)である構文木です。
Go にバリアント型を追加することを検討しましたが、議論の結果、インターフェースと混乱を招く方法で重複するため、追加しないことに決めました。バリアント型の要素がインターフェース自体であった場合、どうなるでしょうか?
また、バリアント型が扱う一部の内容は、すでに言語によってカバーされています。エラーの例は、エラーを保持するインターフェース値とケースを識別するための型スイッチを使用して簡単に表現できます。構文木の例も可能ですが、あまり優雅ではありません。
なぜ Go には共変結果型がないのか?
共変結果型は、次のようなインターフェースを意味します:
type Copyable interface {
Copy() interface{}
}
これは、Value
が空のインターフェースを実装しているため、メソッド bash
func (v Value) Copy() Value
によって満たされます。Go では、メソッドの型は正確に一致する必要があるため、Value
は Copyable
を実装していません。Go は、型が何をするか(そのメソッド)と型の実装を分離します。2つのメソッドが異なる型を返す場合、それらは同じことをしていません。共変結果型を望むプログラマーは、しばしばインターフェースを通じて型階層を表現しようとしています。Go では、インターフェースと実装の間に明確な分離を持つことがより自然です。
値
なぜ Go には暗黙の数値変換がないのか?
C における数値型間の自動変換の便利さは、それが引き起こす混乱を上回ります。式はいつ符号なしですか?値はどれくらいの大きさですか?オーバーフローしますか?結果はポータブルですか?実行されるマシンに依存しませんか?また、コンパイラを複雑にします。C の「通常の算術変換」は実装が難しく、アーキテクチャ間で一貫性がありません。ポータビリティの理由から、いくつかの明示的な変換のコストをかけて、明確で簡潔にすることに決めました。Go における定数の定義は、符号なしおよびサイズ注釈のない任意精度の値であり、かなりの程度で問題を軽減します。
関連する詳細は、C とは異なり、int
と int64
は異なる型であり、int
が 64 ビット型であってもです。int
型はジェネリックです。整数が何ビット保持するかを気にする場合、Go では明示的であることを奨励します。
定数はどのように機能しますか?
Go は異なる数値型の変数間の変換に厳格ですが、言語内の定数ははるかに柔軟です。23
、3.14159
、および math.Pi
のようなリテラル定数は、任意の精度でオーバーフローやアンダーフローのない理想的な数値空間を占有します。たとえば、math.Pi
の値はソースコード内で 63 桁の10進数で指定され、値に関する定数式は float64
が保持できる以上の精度を維持します。定数または定数式が変数に割り当てられるとき(プログラム内のメモリ位置)、それは通常の浮動小数点特性と精度を持つ「コンピュータ」数になります。
また、定数は単なる数値であり、型付き値ではないため、Go の定数は変数よりも自由に使用でき、厳格な変換ルールに関するいくつかの不便さを和らげます。次のような式を書くことができます:
sqrt2 := math.Sqrt(2)
コンパイラからの苦情なしに、理想的な数 2
は float64
に安全かつ正確に変換できます。
「定数」というタイトルのブログ記事 [https://golang.org/blog/constants] は、このトピックをより詳細に探求しています。
なぜマップは組み込まれているのか?
文字列と同じ理由です:それらは非常に強力で重要なデータ構造であり、1つの優れた実装を提供し、構文サポートを行うことでプログラミングをより快適にします。Go のマップの実装は非常に強力であり、ほとんどの使用に対応できると考えています。特定のアプリケーションがカスタム実装から利益を得る場合、実装を書くことは可能ですが、構文的にはそれほど便利ではありません。これは合理的なトレードオフのようです。
なぜマップはスライスをキーとして許可しないのか?
マップの検索には等価演算子が必要ですが、スライスはそれを実装していません。スライスは、等価性がそのような型で明確に定義されていないため、等価性を実装していません。浅い比較と深い比較、ポインタと値の比較、再帰型の扱いなど、複数の考慮事項があります。この問題を再検討するかもしれませんが、スライスの等価性が何を意味するかについて明確なアイデアがない限り、今のところは省略する方が簡単でした。
構造体と配列には等価性が定義されているため、それらはマップのキーとして使用できます。
なぜマップ、スライス、チャネルは参照であり、配列は値なのか?
このトピックには多くの歴史があります。初期には、マップとチャネルは構文的にポインタであり、非ポインタインスタンスを宣言または使用することは不可能でした。また、配列がどのように機能すべきかについても苦労しました。最終的に、ポインタと値の厳格な分離が言語を使いにくくすることがわかりました。これらの型を関連する共有データ構造への参照として機能するように変更することで、これらの問題が解決されました。この変更は言語にいくつかの残念な複雑さを追加しましたが、使いやすさに大きな影響を与えました。Go は導入されたときにより生産的で快適な言語になりました。
コードを書く
ライブラリはどのように文書化されていますか?
コマンドラインからドキュメントにアクセスするには、go ツールに doc サブコマンドがあり、宣言、ファイル、パッケージなどのドキュメントへのテキストインターフェースを提供します。
グローバルパッケージ発見ページ pkg.go.dev/pkg/ は、Go ソースコードからパッケージドキュメントを抽出し、HTML として提供し、宣言や関連要素へのリンクを提供するサーバーを実行します。これは、既存の Go ライブラリについて学ぶ最も簡単な方法です。
プロジェクトの初期には、godoc
という類似のプログラムがあり、ローカルマシン上のファイルのドキュメントを抽出するために実行することもできました。 pkg.go.dev/pkg/ は本質的にその子孫です。別の子孫は、pkgsite
コマンドで、godoc
のようにローカルで実行できますが、go
doc
によって表示される結果にはまだ統合されていません。
Go プログラミングスタイルガイドはありますか?
明示的なスタイルガイドはありませんが、確かに認識可能な「Go スタイル」があります。
Go には、命名、レイアウト、ファイル構成に関する決定を導くための確立された慣習があります。文書 Effective Go には、これらのトピックに関するいくつかのアドバイスが含まれています。より直接的には、プログラム gofmt
は、レイアウトルールを強制することを目的としたプリティプリンタです。これは、解釈を許可する通常のドスとドンツの集大成を置き換えます。リポジトリ内のすべての Go コード、およびオープンソースの世界の大多数は、gofmt
を通過しています。
Go コードレビューコメント というタイトルの文書は、プログラマーが見逃しがちな Go イディオムの詳細に関する非常に短いエッセイのコレクションです。これは、Go プロジェクトのコードレビューを行う人々にとって便利なリファレンスです。
Go ライブラリにパッチを提出するにはどうすればよいですか?
ライブラリのソースは、リポジトリの src
ディレクトリにあります。重要な変更を加えたい場合は、始める前にメーリングリストで議論してください。
手続きについての詳細は、Go プロジェクトへの貢献 の文書を参照してください。
なぜ「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://[email protected]/"]
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
始めるための詳細については、チュートリアル:モジュールを作成する を参照してください。
依存関係をモジュールで管理するためのガイドについては、モジュールの開発 を参照してください。
モジュール内のパッケージは、進化する際に後方互換性を維持する必要があります。これは インポート互換性ルール に従います:
古いパッケージと新しいパッケージが同じインポートパスを持つ場合、
⟩
新しいパッケージは古いパッケージと後方互換性がある必要があります。
Go 1 互換性ガイドライン はここで良いリファレンスです:エクスポートされた名前を削除しない、タグ付き合成リテラルを奨励するなど。異なる機能が必要な場合は、古い名前を変更するのではなく、新しい名前を追加してください。
モジュールは、セマンティックバージョニング とセマンティックインポートバージョニングでこれを体系化します。互換性の破れが必要な場合は、新しいメジャーバージョンでモジュールをリリースします。メジャーバージョン 2 以上のモジュールは、パスの一部として メジャーバージョンサフィックス を必要とします(/v2
のように)。これにより、インポート互換性ルールが保持されます:モジュールの異なるメジャーバージョンのパッケージは異なるパスを持ちます。
ポインタと割り当て
関数パラメータはいつ値渡しされますか?
C 系のすべての言語と同様に、Go ではすべてが値渡しされます。つまり、関数は常に渡されるもののコピーを受け取ります。これは、値をパラメータに割り当てる文があるかのようです。たとえば、int
値を関数に渡すと、int
のコピーが作成され、ポインタ値を渡すとポインタのコピーが作成されますが、ポインタが指すデータはコピーされません。(この影響については、後のセクションを参照してください。)
マップとスライスの値はポインタのように振る舞います。これらは、基になるマップまたはスライスデータへのポインタを含む記述子です。マップまたはスライス値をコピーしても、指しているデータはコピーされません。インターフェース値をコピーすると、インターフェース値に格納されているもののコピーが作成されます。インターフェース値が構造体を保持している場合、インターフェース値をコピーすると構造体のコピーが作成されます。インターフェース値がポインタを保持している場合、インターフェース値をコピーするとポインタのコピーが作成されますが、再び指しているデータはコピーされません。
この議論は操作のセマンティクスに関するものであることに注意してください。実際の実装では、セマンティクスを変更しない限り、コピーを避けるための最適化が適用される場合があります。
インターフェースへのポインタを使用するのはいつですか?
ほとんどありません。インターフェース値へのポインタは、インターフェース値の型を隠すための遅延評価に関与するまれで難しい状況でのみ発生します。
インターフェースを期待する関数にインターフェース値へのポインタを渡すのは一般的な間違いです。コンパイラはこのエラーについて文句を言いますが、時には ポインタがインターフェースを満たすために必要です ので、状況は混乱を招く可能性があります。洞察は、具体的な型へのポインタはインターフェースを満たすことができるが、1つの例外として インターフェースへのポインタは決してインターフェースを満たすことができない ということです。
変数宣言を考えてみましょう、
var w io.Writer
印刷関数 fmt.Fprintf
は、最初の引数として io.Writer
を満たす値を取ります。これは、標準の Write
メソッドを実装する何かです。したがって、次のように書くことができます:
fmt.Fprintf(w, "hello, world\n")
しかし、w
のアドレスを渡すと、プログラムはコンパイルされません。
fmt.Fprintf(&w, "hello, world\n") // Compile-time error.
唯一の例外は、空のインターフェース型(interface{}
)の変数に、任意の値、ポインタを含む値を割り当てることができることです。それでも、値がインターフェースへのポインタである場合はほぼ確実に間違いです。その結果は混乱を招く可能性があります。
値またはポインタにメソッドを定義すべきか?
func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value
ポインタに慣れていないプログラマーにとって、これら2つの例の違いは混乱を招く可能性がありますが、実際の状況は非常にシンプルです。型にメソッドを定義する際、レシーバー(上記の例では s
)は、メソッドへの引数のように振る舞います。したがって、レシーバーを値として定義するかポインタとして定義するかは、関数引数を値またはポインタにすべきかという同じ質問です。いくつかの考慮事項があります。
まず最も重要なのは、メソッドがレシーバーを変更する必要があるかどうかです。必要な場合、レシーバーは ポインタ でなければなりません。(スライスとマップは参照として機能するため、彼らの話は少し微妙ですが、たとえば、メソッド内でスライスの長さを変更するには、レシーバーは依然としてポインタでなければなりません。)上記の例では、pointerMethod
が s
のフィールドを変更すると、呼び出し元はその変更を確認できますが、valueMethod
は呼び出し元の引数のコピーで呼び出されるため(それが値を渡す定義です)、呼び出し元には変更が見えません。
ちなみに、Java ではメソッドレシーバーは常にポインタですが、そのポインタの性質はやや隠されています(最近の開発により、Java に値レシーバーが導入されています)。Go では値レシーバーが異常です。
次に効率の考慮があります。レシーバーが大きい場合、たとえば大きな struct
の場合、ポインタレシーバーを使用する方が安価かもしれません。
次は一貫性です。型の一部のメソッドがポインタレシーバーを持つ必要がある場合、残りもそうすべきです。そうすれば、型がどのように使用されるかに関係なく、メソッドセットが一貫性を持ちます。詳細については、メソッドセット のセクションを参照してください。
基本型、スライス、小さな structs
のような型の場合、値レシーバーは非常に安価です。したがって、メソッドのセマンティクスがポインタを必要としない限り、値レシーバーは効率的で明確です。
new と make の違いは何ですか?
要するに:new
はメモリを割り当て、make
はスライス、マップ、チャネル型を初期化します。
詳細については、Effective Go の関連セクション を参照してください。
64 ビットマシンでの int のサイズはどのくらいですか?
int
と uint
のサイズは実装に依存しますが、特定のプラットフォームでは同じです。ポータビリティのために、特定のサイズの値に依存するコードは、int64
のような明示的にサイズ指定された型を使用するべきです。32 ビットマシンでは、コンパイラはデフォルトで 32 ビット整数を使用しますが、64 ビットマシンでは整数は 64 ビットです。(歴史的に、これは常に真ではありませんでした。)
一方、浮動小数点スカラーと複素型は常にサイズが指定されています(float
や complex
の基本型はありません)。プログラマーは浮動小数点数を使用する際に精度を意識する必要があります。未型付けの浮動小数点定数に使用されるデフォルト型は float64
です。したがって、foo
:=
3.0
は、foo
型の変数 float64
を宣言します。未型付けの定数で初期化された float32
変数の場合、変数の型は変数宣言で明示的に指定する必要があります:
var foo float32 = 3.0
または、定数は foo := float32(3.0)
のように変換を伴って型を与えなければなりません。
変数がヒープまたはスタックに割り当てられているかどうかはどうやってわかりますか?
正確性の観点からは、知る必要はありません。Go の各変数は、それに対する参照がある限り存在します。実装によって選択されたストレージ位置は、言語のセマンティクスには関係ありません。
ストレージ位置は、効率的なプログラムを書くことに影響を与えます。可能な場合、Go コンパイラは関数にローカルな変数をその関数のスタックフレームに割り当てます。ただし、コンパイラが関数が返された後に変数が参照されないことを証明できない場合、コンパイラはダングリングポインタエラーを避けるために変数をガベージコレクションされたヒープに割り当てなければなりません。また、ローカル変数が非常に大きい場合、スタックよりもヒープに保存する方が理にかなっているかもしれません。
現在のコンパイラでは、変数のアドレスが取得されると、その変数はヒープに割り当てられる候補となります。ただし、基本的な エスケープ解析 は、関数からの戻り後にそのような変数が生存しない場合に、スタックに存在できる場合を認識します。
なぜ私の Go プロセスはこれほど多くの仮想メモリを使用するのか?
Go メモリアロケータは、割り当てのためのアリーナとして大きな仮想メモリ領域を予約します。この仮想メモリは特定の Go プロセスにローカルであり、予約は他のプロセスからメモリを奪うものではありません。
Go プロセスに実際に割り当てられたメモリの量を見つけるには、Unix top
コマンドを使用し、RES
(Linux)または RSIZE
(macOS)列を参照してください。
並行性
どの操作が原子的ですか?ミューテックスはどうですか?
Go における操作の原子性の説明は、Go メモリモデル 文書にあります。
低レベルの同期と原子的なプリミティブは、sync および sync/atomic パッケージで利用可能です。これらのパッケージは、参照カウントのインクリメントや小規模な相互排除を保証するなどの簡単なタスクに適しています。
より高レベルの操作、たとえば並行サーバー間の調整などは、より良いプログラムを生み出すことができ、高レベルの技術を通じて Go はこのアプローチをサポートします。たとえば、プログラムを構成して、特定のデータの責任を持つのは常に1つのゴルーチンだけにすることができます。このアプローチは、元の Go のことわざ に要約されています。
メモリを共有するのではなく、通信によってメモリを共有してください。
この概念の詳細な議論については、通信によるメモリの共有 コードウォークとその 関連する記事 を参照してください。
大規模な並行プログラムは、これらのツールキットの両方から借りる可能性が高いです。
なぜプログラムは CPU を増やしても速くならないのか?
プログラムが CPU を増やしても速くなるかどうかは、解決しようとしている問題に依存します。Go 言語は goroutine やチャネルなどの並行性のプリミティブを提供しますが、並行性は基礎となる問題が本質的に並列である場合にのみ並列性を可能にします。本質的に逐次的な問題は、CPU を追加しても速度が向上することはありませんが、並列に実行できる部分に分割できる問題は、時には劇的に速度が向上することがあります。
時には、CPU を追加することでプログラムが遅くなることもあります。実際のところ、役に立つ計算を行うよりも同期や通信に多くの時間を費やすプログラムは、複数の OS スレッドを使用する際にパフォーマンスが低下する可能性があります。これは、スレッド間でデータを渡すことがコンテキストスイッチを伴い、そのコストが CPU が増えると増加するためです。たとえば、Go の仕様からの 素数ふるいの例 は、多くの goroutine を起動しますが、重要な並列性はありません。スレッド (CPU) の数を増やすことは、速度を上げるよりも遅くする可能性が高いです。
このトピックの詳細については、Concurrency is not Parallelism というタイトルのトークを参照してください。
CPU の数を制御するにはどうすればよいですか?
実行中の goroutine に同時に利用可能な CPU の数は、GOMAXPROCS
シェル環境変数によって制御され、そのデフォルト値は利用可能な CPU コアの数です。したがって、並列実行の可能性があるプログラムは、デフォルトでマルチ CPU マシンで実行されるべきです。使用する並列 CPU の数を変更するには、環境変数を設定するか、ランタイムパッケージの同様の名前の 関数 を使用して、異なるスレッド数を利用するようにランタイムサポートを構成します。これを 1 に設定すると、真の並列性の可能性が排除され、独立した goroutine が交互に実行されることになります。
ランタイムは、複数の未処理の I/O リクエストにサービスを提供するために、GOMAXPROCS
の値よりも多くのスレッドを割り当てることができます。GOMAXPROCS
は、実際に同時に実行できる goroutine の数にのみ影響します。任意の数の goroutine がシステムコールでブロックされる可能性があります。
Go の goroutine スケジューラは、goroutine とスレッドのバランスをうまく取ることができ、同じスレッド上の他の goroutine が飢えないように、goroutine の実行を事前に中断することもできます。しかし、完璧ではありません。パフォーマンスの問題が見られる場合は、アプリケーションごとに GOMAXPROCS
を設定することで改善されるかもしれません。
なぜ goroutine ID がないのか?
Goroutine には名前がありません。彼らは単なる匿名のワーカーです。プログラマーに対してユニークな識別子、名前、またはデータ構造を公開しません。このことに驚く人もいますが、go
ステートメントが後で goroutine にアクセスして制御するために使用できるアイテムを返すことを期待しています。
Goroutine が匿名である根本的な理由は、並行コードをプログラミングする際に Go 言語全体が利用可能であることです。対照的に、スレッドや goroutine に名前を付けると、これらを使用するライブラリができる使用パターンが制限される可能性があります。
以下は、その困難さの例です。goroutine に名前を付けてそれに基づいてモデルを構築すると、それは特別なものになり、その goroutine にすべての計算を関連付ける誘惑に駆られ、処理のために複数の共有 goroutine を使用する可能性を無視してしまいます。もし net/http
パッケージがリクエストごとの状態を goroutine に関連付けていた場合、クライアントはリクエストを処理する際により多くの goroutine を使用できなくなります。
さらに、すべての処理が「メインスレッド」で行われる必要があるグラフィックスシステムなどのライブラリの経験は、並行言語で展開されたときにこのアプローチがどれほど不便で制限的であるかを示しています。特別なスレッドや goroutine の存在は、プログラマーに誤って間違ったスレッドで操作することによって引き起こされるクラッシュやその他の問題を避けるためにプログラムを歪めることを強いることになります。
特定の goroutine が本当に特別な場合には、言語はそれと柔軟に対話するために使用できるチャネルなどの機能を提供します。
関数とメソッド
なぜ T と *T には異なるメソッドセットがあるのか?
Go 仕様 によれば、型 T
のメソッドセットは、レシーバ型 T
を持つすべてのメソッドで構成され、対応するポインタ型 *T
のメソッドセットは、レシーバ *T
または T
を持つすべてのメソッドで構成されます。つまり、*T
のメソッドセットには T
のメソッドセットが含まれますが、その逆は含まれません。
この区別は、インターフェース値がポインタ *T
を含む場合、メソッド呼び出しがポインタをデリファレンスすることによって値を取得できるのに対し、インターフェース値が値 T
を含む場合、メソッド呼び出しがポインタを取得する安全な方法がないために生じます。(そうすることは、メソッドがインターフェース内の値の内容を変更することを許可することになり、これは言語仕様によって許可されていません。)
コンパイラが値のアドレスを取得してメソッドに渡すことができる場合でも、メソッドが値を変更すると、変更は呼び出し元で失われます。
たとえば、以下のコードが有効であった場合:
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
標準入力を buf
の コピー にコピーし、buf
自体にはコピーしません。これはほとんど望ましい動作ではなく、したがって言語によって禁止されています。
クロージャが goroutine として実行されるとどうなるか?
ループ変数の動作のために、Go バージョン 1.22 より前(このセクションの最後に更新があります)では、並行性を持つクロージャを使用する際に混乱が生じる可能性があります。次のプログラムを考えてみてください:
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
}
}
出力として a, b, c
が表示されると誤って期待するかもしれません。実際に見ることになるのは c, c, c
です。これは、ループの各反復が変数 v
の同じインスタンスを使用するため、各クロージャがその単一の変数を共有するためです。クロージャが実行されると、fmt.Println
が実行される時点での v
の値を印刷しますが、goroutine が起動されてから v
が変更されている可能性があります。この問題や他の問題を事前に検出するために、go vet
を実行してください。
v
の現在の値を各クロージャにバインドするには、内側のループを修正して各反復で新しい変数を作成する必要があります。1つの方法は、変数をクロージャへの引数として渡すことです:
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}
この例では、v
の値が匿名関数への引数として渡されます。その値は、関数内で変数 u
としてアクセス可能です。
さらに簡単なのは、新しい変数を作成することです。これは、Go では奇妙に見えるかもしれませんが、うまく機能します:
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
この言語の動作、すなわち各反復のために新しい変数を定義しないことは、振り返ってみると誤りと見なされ、Go 1.22 で対処され、各反復のために新しい変数が作成され、この問題が解消されました。
制御フロー
なぜ Go には ?: 演算子がないのか?
Go には三項テスト演算子がありません。次のようにして同じ結果を得ることができます:
if expr {
n = trueVal
} else {
n = falseVal
}
?:
が Go に存在しない理由は、言語の設計者がこの演算子があまりにも頻繁に使用され、理解しにくい複雑な式を作成することを見てきたからです。if-else
形式は、長いですが、間違いなく明確です。言語には、1 つの条件付き制御フロー構造だけが必要です。
型パラメータ
なぜ Go には型パラメータがあるのか?
型パラメータは、関数やデータ構造が後で指定される型に基づいて定義される、いわゆるジェネリックプログラミングを可能にします。たとえば、これにより、異なる順序型の 2 つの値の最小値を返す関数を書くことが可能になり、各可能な型のために別のバージョンを書く必要がなくなります。詳細な説明と例については、ブログ記事 Why Generics? を参照してください。
Go でのジェネリックはどのように実装されていますか?
コンパイラは、各インスタンス化を個別にコンパイルするか、類似のインスタンス化を単一の実装としてコンパイルするかを選択できます。単一の実装アプローチは、インターフェースパラメータを持つ関数に似ています。異なるコンパイラは、異なるケースに対して異なる選択をします。標準の Go コンパイラは、通常、同じ形状のすべての型引数に対して単一のインスタンス化を出力します。形状は、型が持つポインタのサイズや位置などの特性によって決まります。将来のリリースでは、コンパイル時間、実行時効率、コードサイズのトレードオフを実験する可能性があります。
Go のジェネリックは他の言語のジェネリックとどのように比較されますか?
すべての言語における基本的な機能は似ています:後で指定される型を使用して型や関数を書くことが可能です。ただし、いくつかの違いがあります。
- Java
Java では、コンパイラはコンパイル時にジェネリック型をチェックしますが、実行時には型を削除します。これは 型消去 と呼ばれます。たとえば、コンパイル時にList<Integer>
として知られる Java 型は、実行時には非ジェネリック型List
になります。これは、たとえば、Java の型反射形式を使用する際に、List<Integer>
型の値とList<Float>
型の値を区別することが不可能であることを意味します。Go では、ジェネリック型の反射情報には、完全なコンパイル時型情報が含まれます。Java は、List<? extends Number>
やList<? super Number>
のような型ワイルドカードを使用して、ジェネリック共変性と反変性を実装します。Go にはこれらの概念がなく、Go のジェネリック型ははるかにシンプルです。 - C++
従来、C++ テンプレートは型引数に対して制約を強制しませんでしたが、C++20 では 概念 を介してオプションの制約をサポートしています。Go では、すべての型パラメータに対して制約が必須です。C++20 の概念は、型引数でコンパイルされる必要がある小さなコードフラグメントとして表現されます。Go の制約は、許可される型引数のセットを定義するインターフェース型です。C++ はテンプレートメタプログラミングをサポートしますが、Go はサポートしません。実際、すべての C++ コンパイラは、インスタンス化されるポイントで各テンプレートをコンパイルします。上記のように、Go は異なるインスタンス化に対して異なるアプローチを使用できます。 - Rust
Rust の制約のバージョンはトレイト境界として知られています。Rust では、トレイト境界と型の関連付けは、トレイト境界を定義するクレートまたは型を定義するクレートのいずれかで明示的に定義する必要があります。Go では、型引数は暗黙的に制約を満たします。Go の型は暗黙的にインターフェース型を実装します。Rust の標準ライブラリは、比較や加算などの操作のための標準トレイトを定義していますが、Go の標準ライブラリにはこれがなく、これらはインターフェース型を介してユーザーコードで表現できます。唯一の例外は、Go のcomparable
事前定義インターフェースであり、これは型システムでは表現できない特性をキャプチャします。 - Python
Python は静的型付け言語ではないため、すべての Python 関数は常にデフォルトでジェネリックであると言えます:それらは常に任意の型の値で呼び出すことができ、型エラーは実行時に検出されます。
なぜ Go では型パラメータリストに角括弧を使用するのか?
Java と C++ は、型パラメータリストに角括弧を使用します。たとえば、Java の List<Integer>
や C++ の std::vector<int>
のように。しかし、そのオプションは Go には利用できません。なぜなら、関数内のコードを解析する際に、v := F<T>
のように、<
を見た時点で、インスタンス化を見ているのか、<
演算子を使用した式を見ているのかが曖昧になるからです。これは型情報なしでは非常に解決が難しいです。
たとえば、次のような文を考えてみてください。
a, b = w < x, y > (z)
型情報がなければ、代入の右辺が式のペア (w < x
と y > z
) であるのか、2 つの結果値を返すジェネリック関数のインスタンス化と呼び出しであるのかを決定することは不可能です ((w<x, y>)(z)
)。
Go の重要な設計決定は、型情報なしで解析が可能であることです。これは、ジェネリックに角括弧を使用する場合には不可能に思えます。
Go は、角括弧を使用する点で独自ではありません。Scala のように、ジェネリックコードに角括弧を使用する他の言語もあります。
なぜ Go では型パラメータを持つメソッドをサポートしていないのか?
Go は、ジェネリック型がメソッドを持つことを許可しますが、レシーバを除いて、これらのメソッドの引数はパラメータ化された型を使用できません。Go がジェネリックメソッドを追加することはないと予想しています。
問題は、それらをどのように実装するかです。具体的には、インターフェース内の値が追加のメソッドを持つ別のインターフェースを実装しているかどうかをチェックすることを考えてみてください。たとえば、次の型を考えてみてください。これは、任意の可能な型の引数を返すジェネリック Nop
メソッドを持つ空の構造体です。
type Empty struct{}
func (Empty) Nop[T any](x T) T {
return x
}
今、Empty
値が any
に格納され、他のコードに渡されて、そのコードが何をできるかをチェックします。
func TryNops(x any) {
if x, ok := x.(interface{ Nop(string) string }); ok {
fmt.Printf("string %s\n", x.Nop("hello"))
}
if x, ok := x.(interface{ Nop(int) int }); ok {
fmt.Printf("int %d\n", x.Nop(42))
}
if x, ok := x.(interface{ Nop(io.Reader) io.Reader }); ok {
data, err := io.ReadAll(x.Nop(strings.NewReader("hello world")))
fmt.Printf("reader %q %v\n", data, err)
}
}
x
が Empty
の場合、そのコードはどのように機能しますか?x
は、すべての 3 つのテストを満たす必要があるようです。他の型の他の形式でも同様です。
これらのメソッドが呼び出されるとき、どのコードが実行されますか?非ジェネリックメソッドの場合、コンパイラはすべてのメソッド実装のコードを生成し、それらを最終プログラムにリンクします。しかし、ジェネリックメソッドの場合、無限の数のメソッド実装が存在する可能性があるため、異なる戦略が必要です。
4 つの選択肢があります:
- 1. リンク時に、すべての可能な動的インターフェースチェックのリストを作成し、それを満たす型を探し、コンパイルされたメソッドが欠けている場合は、コンパイラを再呼び出してそれらのメソッドを追加します。
これにより、リンク後に停止して一部のコンパイルを繰り返す必要があるため、ビルドが大幅に遅くなります。特にインクリメンタルビルドが遅くなります。さらに、新しくコンパイルされたメソッドコード自体が新しい動的インターフェースチェックを持つ可能性があり、そのプロセスを繰り返す必要があります。プロセスが完了しない例を構築できます。 - 2. JIT を実装し、実行時に必要なメソッドコードをコンパイルします。
Go は、純粋に事前コンパイルされたことのシンプルさと予測可能なパフォーマンスから大きな恩恵を受けています。1 つの言語機能を実装するために JIT の複雑さを引き受けることには消極的です。 - 3. 各ジェネリックメソッドに対して遅いフォールバックを発生させ、型パラメータに対するすべての可能な言語操作の関数のテーブルを使用し、そのフォールバック実装を動的テストに使用します。
このアプローチでは、予期しない型によってパラメータ化されたジェネリックメソッドが、コンパイル時に観察された型によってパラメータ化された同じメソッドよりも遅くなります。これにより、パフォーマンスが予測不可能になります。 - 4. ジェネリックメソッドはインターフェースを満たすために使用できないと定義します。
インターフェースは Go のプログラミングにおいて不可欠な部分です。ジェネリックメソッドがインターフェースを満たすことを禁止することは、設計上受け入れられません。
これらの選択肢のいずれも良いものではないため、「上記のいずれでもない」を選択しました。
型パラメータを持つメソッドの代わりに、型パラメータを持つトップレベル関数を使用するか、レシーバ型に型パラメータを追加します。
詳細や例については、提案 を参照してください。
なぜパラメータ化された型のレシーバにより具体的な型を使用できないのか?
ジェネリック型のメソッド宣言は、型パラメータ名を含むレシーバで記述されます。呼び出しサイトで型を指定する構文の類似性のために、特定の型をレシーバに名前を付けることによって、特定の型引数にカスタマイズされたメソッドを生成するメカニズムを提供すると思う人もいます。たとえば、string
のように:
type S[T any] struct { f T }
func (s S[string]) Add(t string) string {
return s.f + t
}
これは失敗します。なぜなら、string
という単語は、コンパイラによってメソッド内の型引数の名前として取られるからです。コンパイラエラーメッセージは「operator + not defined on s.f (variable of type string)
」のようになります。これは、+
演算子が事前宣言された型 string
で正常に機能するため、混乱を招く可能性がありますが、宣言はこのメソッドに対して string
の定義を上書きしており、その演算子は string
の無関係なバージョンでは機能しません。このように事前宣言された名前を上書きすることは有効ですが、奇妙なことをすることであり、しばしば間違いです。
なぜコンパイラは私のプログラムの型引数を推論できないのか?
プログラマーがジェネリック型や関数の型引数が何であるべきかを簡単に見分けられるケースは多くありますが、言語はコンパイラがそれを推論することを許可していません。型推論は意図的に制限されており、どの型が推論されるかについて混乱が生じないようにしています。他の言語の経験から、予期しない型推論はプログラムを読む際やデバッグする際にかなりの混乱を引き起こす可能性があることが示唆されています。呼び出しに使用される明示的な型引数を指定することは常に可能です。将来的には、ルールがシンプルで明確な限り、新しい推論形式がサポートされる可能性があります。
パッケージとテスト
マルチファイルパッケージを作成するにはどうすればよいですか?
パッケージのすべてのソースファイルを、単独のディレクトリに置いてください。ソースファイルは、異なるファイルのアイテムを自由に参照できます。前方宣言やヘッダーファイルは必要ありません。
複数のファイルに分割されている以外は、パッケージは単一ファイルのパッケージと同様にコンパイルおよびテストされます。
ユニットテストを書くにはどうすればよいですか?
パッケージソースと同じディレクトリに _test.go
で終わる新しいファイルを作成します。そのファイル内で、import "testing"
と書き、次の形式の関数を作成します。
func TestFoo(t *testing.T) {
...
}
そのディレクトリで go test
を実行します。そのスクリプトは Test
関数を見つけ、テストバイナリをビルドし、それを実行します。
詳細については、Go コードの書き方 ドキュメント、testing
パッケージ、および go test
サブコマンドを参照してください。
私のお気に入りのテスト用ヘルパー関数はどこにありますか?
Go の標準 testing
パッケージはユニットテストを書くのを簡単にしますが、アサーション関数など、他の言語のテストフレームワークで提供される機能が欠けています。このドキュメントの 以前のセクション では、Go にアサーションがない理由を説明しており、同じ理由が assert
のテストでの使用にも当てはまります。適切なエラーハンドリングは、1 つのテストが失敗した後に他のテストを実行できるようにすることを意味します。これにより、失敗をデバッグする人が何が間違っているのかを完全に把握できます。テストが isPrime
が 2、3、5、7(または 2、4、8、16)に対して間違った答えを返すと報告する方が、isPrime
が 2 に対して間違った答えを返すと報告し、その後のテストが実行されなかったと報告するよりも、はるかに有用です。テスト失敗を引き起こすプログラマーは、失敗したコードに精通していない可能性があります。良いエラーメッセージを書くために投資した時間は、テストが壊れたときに後で報われます。
関連する点は、テストフレームワークは条件文や制御、印刷メカニズムを持つミニ言語に発展する傾向がありますが、Go にはすでにすべての機能があります。なぜそれらを再作成するのか?私たちは Go でテストを書く方が良いと考えています。学ぶ言語が 1 つ減り、アプローチがテストを簡潔で理解しやすく保ちます。
良いエラーを書くために必要な追加コードの量が繰り返しで圧倒的に感じられる場合、テストはテーブル駆動型の方がうまく機能するかもしれません。データ構造で定義された入力と出力のリストを反復処理します(Go はデータ構造リテラルに優れたサポートを提供しています)。良いテストと良いエラーメッセージを書くための作業は、複数のテストケースにわたって分散されます。標準の Go ライブラリには、fmt
パッケージのフォーマットテスト などの説明的な例が満載です。
なぜ標準ライブラリに X がないのか?
標準ライブラリの目的は、ランタイムライブラリをサポートし、オペレーティングシステムに接続し、フォーマットされた I/O やネットワーキングなど、多くの Go プログラムが必要とする重要な機能を提供することです。また、暗号化や HTTP、JSON、XML などの標準をサポートするために、Web プログラミングに重要な要素も含まれています。
何が含まれるかを定義する明確な基準はありません。なぜなら、長い間、これが 唯一の Go ライブラリだったからです。しかし、今日追加されるものを定義する基準はあります。
標準ライブラリへの新しい追加はまれであり、追加のハードルは高いです。標準ライブラリに含まれるコードは、大きな継続的なメンテナンスコストを伴い(しばしば元の著者以外の人が負担します)、Go 1 互換性の約束 の対象となり(API の欠陥を修正することを妨げます)、Go の リリーススケジュール の対象となり、バグ修正がユーザーに迅速に提供されることを妨げます。
ほとんどの新しいコードは標準ライブラリの外に存在し、go
ツール の go get
コマンドを介してアクセス可能であるべきです。そのようなコードには独自のメンテナがあり、リリースサイクルや互換性の保証があります。ユーザーは pkg.go.dev でパッケージを見つけ、ドキュメントを読むことができます。
標準ライブラリには、log/syslog
のように本当に属さない部分もありますが、Go 1 互換性の約束のため、ライブラリ内のすべてを維持し続けます。しかし、ほとんどの新しいコードは他の場所に存在することを奨励します。
実装
コンパイラを構築するために使用されるコンパイラ技術は何ですか?
Go にはいくつかのプロダクションコンパイラがあり、さまざまなプラットフォーム向けに開発中の他のコンパイラもあります。
デフォルトのコンパイラである gc
は、go
コマンドのサポートの一部として Go 配布に含まれています。Gc
は、ブートストラップの難しさから C で最初に書かれました。Go 環境をセットアップするには Go コンパイラが必要です。しかし、状況は進展し、Go 1.5 リリース以降、コンパイラは Go プログラムになりました。コンパイラは C から Go に自動翻訳ツールを使用して変換されました。この 設計文書 と トーク で説明されています。したがって、コンパイラは現在「自己ホスティング」であり、ブートストラップの問題に直面する必要があります。解決策は、通常の C インストールと同様に、すでに動作している Go インストールを持つことです。ソースから新しい Go 環境を立ち上げる方法については、こちら と こちら を参照してください。
Gc
は、再帰的下降パーサーを持つ Go で書かれており、Plan 9 ローダーに基づいたカスタムローダーを使用して ELF/Mach-O/PE バイナリを生成します。
Gccgo
コンパイラは、再帰的下降パーサーを持つ C++ で書かれ、標準 GCC バックエンドに接続されています。実験的な LLVM バックエンド は、同じフロントエンドを使用しています。
プロジェクトの初めに、gc
に LLVM を使用することを検討しましたが、パフォーマンス目標を達成するには大きすぎて遅すぎると判断しました。振り返ってみると、LLVM から始めることは、Go が必要とする ABI や関連する変更(スタック管理など)を導入するのを難しくすることになったでしょうが、これらは標準 C セットアップの一部ではありませんでした。
Go は、Go コンパイラを実装するのに適した言語であることが判明しましたが、それが元々の目標ではありませんでした。最初から自己ホスティングでないことは、Go の設計が元々のユースケース、すなわちネットワークサーバーに集中することを可能にしました。もし最初に Go が自分自身をコンパイルするべきだと決定していたら、コンパイラ構築向けにターゲットを絞った言語になっていたかもしれません。それは価値のある目標ですが、最初に持っていた目標ではありませんでした。
gc
には独自の実装がありますが、ネイティブの字句解析器と構文解析器が go/parser
パッケージで利用可能であり、ネイティブの 型チェッカー もあります。gc
コンパイラは、これらのライブラリのバリアントを使用します。
ランタイムサポートはどのように実装されていますか?
再びブートストラップの問題により、ランタイムコードは元々 C で主に書かれていました(わずかにアセンブラも含む)が、その後 Go に翻訳されました(アセンブラの一部を除く)。Gccgo
のランタイムサポートは glibc
を使用します。gccgo
コンパイラは、最近の gold リンカーの変更によってサポートされるセグメントスタックと呼ばれる技術を使用して goroutine を実装します。Gollvm
も同様に、対応する LLVM インフラストラクチャに基づいて構築されています。
なぜ私のトリビアルなプログラムはそんなに大きなバイナリなのか?
gc
ツールチェーンのリンカーは、デフォルトで静的リンクされたバイナリを作成します。したがって、すべての Go バイナリには Go ランタイムが含まれ、動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要なランタイム型情報が含まれます。
C でコンパイルされ、静的にリンクされた「こんにちは、世界」プログラムは、約 750 kB で、printf
の実装が含まれています。同等の Go プログラムは fmt.Printf
を使用しており、数メガバイトのサイズですが、より強力なランタイムサポートと型およびデバッグ情報が含まれています。
gc
でコンパイルされた Go プログラムは、-ldflags=-w
フラグでリンクされ、DWARF 生成を無効にし、バイナリからデバッグ情報を削除しますが、他の機能の損失はありません。これにより、バイナリサイズを大幅に削減できます。
未使用の変数/インポートに関するこれらの苦情を止めることはできますか?
未使用の変数が存在することはバグを示す可能性があり、未使用のインポートはコンパイルを遅くするだけであり、プログラムが時間とともにコードやプログラマーを蓄積するにつれて、かなりの影響を及ぼす可能性があります。これらの理由から、Go は未使用の変数やインポートを持つプログラムのコンパイルを拒否し、短期的な便利さを長期的なビルド速度とプログラムの明確さと交換します。
それでも、コードを開発しているときに、これらの状況を一時的に作成することは一般的であり、プログラムがコンパイルされる前にそれらを編集しなければならないのは面倒です。
一部の人々は、これらのチェックをオフにするか、少なくとも警告に減らすコンパイラオプションを求めています。そのようなオプションは追加されていませんが、コンパイラオプションは言語の意味に影響を与えるべきではなく、Go コンパイラは警告を報告せず、コンパイルを妨げるエラーのみを報告します。
警告がない理由は 2 つあります。まず、文句を言う価値があるなら、コードで修正する価値があります。(逆に、修正する価値がないなら、言及する価値もありません。)第二に、コンパイラが警告を生成すると、実装が弱いケースについて警告することを奨励し、コンパイルをノイズで覆い隠し、実際のエラーを隠す可能性があります。
状況に対処するのは簡単です。開発中に未使用のものを保持するためにブランク識別子を使用します。
import "unused"
// This declaration marks the import as used by referencing an
// item from the package.
var _ = unused.Item // TODO: Delete before committing!
func main() {
debugData := debug.Profile()
_ = debugData // Used only during debugging.
....
}
現在、ほとんどの Go プログラマーは、goimports というツールを使用しており、これは Go ソースファイルを自動的に書き換えて正しいインポートを持つようにし、実際に未使用のインポートの問題を排除します。このプログラムは、Go ソースファイルが書き込まれるときに自動的に実行されるように、ほとんどのエディタや IDE に簡単に接続できます。この機能は、gopls
にも組み込まれており、上記 で説明されています。
なぜ私のウイルススキャンソフトウェアは、私の Go 配布またはコンパイルされたバイナリが感染していると思うのか?
これは一般的な現象であり、特に Windows マシンでよく見られ、ほとんど常に誤検知です。商業的なウイルススキャンプログラムは、他の言語からコンパイルされたバイナリほど頻繁に見られない Go バイナリの構造に混乱することがよくあります。
Go 配布をインストールしたばかりで、システムが感染していると報告している場合、それは確かに間違いです。徹底的に確認するために、ダウンロードページ のチェックサムと比較してダウンロードを確認できます。
いずれにせよ、報告が誤りであると思われる場合は、ウイルススキャナーの供給者にバグを報告してください。おそらく、ウイルススキャナーは Go プログラムを理解することを学ぶことができるでしょう。
パフォーマンス
なぜGoはベンチマークXで悪いパフォーマンスを示すのか?
Goの設計目標の一つは、比較可能なプログラムに対してCのパフォーマンスに近づくことですが、いくつかのベンチマークではかなり悪い結果を示します。特に、golang.org/x/exp/shootoutに含まれるいくつかのベンチマークが該当します。最も遅いものは、Goで同等のパフォーマンスのバージョンが利用できないライブラリに依存しています。例えば、pidigits.goは多倍長数学パッケージに依存しており、CのバージョンはGoのものとは異なり、最適化されたアセンブラで書かれたGMPを使用しています。正規表現に依存するベンチマーク(例えば、regex-dna.go)は、基本的にGoのネイティブregexpパッケージと、PCREのような成熟した高最適化の正規表現ライブラリを比較しています。
ベンチマークゲームは広範な調整によって勝敗が決まりますが、ほとんどのベンチマークのGoバージョンは注意が必要です。真に比較可能なCとGoのプログラムを測定すると(reverse-complement.goが一例です)、このスイートが示すよりも、両言語は生のパフォーマンスにおいてはるかに近いことがわかります。
それでも、改善の余地はあります。コンパイラは良いですが、さらに良くなる可能性があります。多くのライブラリは大幅なパフォーマンス向上が必要であり、ガーベジコレクタはまだ十分に速くありません。(たとえ速くなったとしても、不必要なガーベジを生成しないように注意することは大きな影響を与える可能性があります。)
いずれにせよ、Goはしばしば非常に競争力があります。言語とツールが発展するにつれて、多くのプログラムのパフォーマンスが大幅に改善されました。Goプログラムのプロファイリングに関するブログ記事を参照すると、有益な例が見つかります。これはかなり古いですが、今でも役立つ情報が含まれています。
Cからの変更点
なぜ構文がCとこれほど異なるのか?
宣言構文を除けば、違いは大きくなく、二つの欲求から生じています。第一に、構文は軽快に感じられるべきであり、必須のキーワードや繰り返し、難解な要素が多すぎてはいけません。第二に、この言語は解析が容易になるように設計されており、シンボルテーブルなしでパース可能です。これにより、デバッガ、依存関係解析ツール、自動ドキュメント抽出ツール、IDEプラグインなどのツールを構築するのがはるかに簡単になります。Cおよびその子孫は、この点で notoriously 難しいです。
なぜ宣言が逆になっているのか?
Cに慣れている場合、逆になっていると感じるかもしれません。Cでは、変数はその型を示す式のように宣言されるという考え方があります。これは良いアイデアですが、型と式の文法はうまく混ざり合わず、結果が混乱を招くことがあります。関数ポインタを考えてみてください。Goは主に式と型の構文を分離しており、これにより物事が簡素化されます(ポインタのために接頭辞*
を使用するのは例外ですが、これはルールを証明する例です)。Cでは、宣言
int* a, b;
declares a
をポインタとして宣言しますが、b
はそうではありません。一方、Goでは
var a, b *int
declares 両方をポインタとして宣言します。これはより明確で規則的です。また、:=
の短い宣言形式は、完全な変数宣言が:=
と同じ順序を示すべきであると主張しています。したがって、
var a uint64 = 1
は
a := uint64(1)
と同じ効果を持ちます。型のための明確な文法を持つことで、パースも簡素化されます。func
やchan
のようなキーワードが明確さを保ちます。
詳細については、Goの宣言構文に関する記事を参照してください。
なぜポインタ算術がないのか?
安全性です。ポインタ算術がないことで、誤って不正なアドレスを導出することが決してない言語を作成することが可能です。コンパイラとハードウェア技術は進歩し、配列インデックスを使用するループはポインタ算術を使用するループと同じくらい効率的になることができます。また、ポインタ算術がないことで、ガーベジコレクタの実装が簡素化される可能性があります。
なぜ++と—は文であり、式ではないのか?そして、なぜ後置で、前置ではないのか?
ポインタ算術がないため、前置および後置インクリメント演算子の便利さは低下します。それらを式階層から完全に削除することで、式の構文が簡素化され、++
や--
の評価順序に関する厄介な問題(f(i++)
やp[i] = q[++i]
を考えてみてください)も排除されます。この簡素化は重要です。後置と前置のどちらも機能しますが、後置バージョンの方が伝統的です。前置に対する主張は、皮肉にも後置インクリメントを含む言語のライブラリであるSTLから生じました。
なぜ波括弧があるのにセミコロンがないのか?そして、なぜ開き波括弧を次の行に置けないのか?
Goは文のグループ化のために波括弧を使用します。この構文はCファミリーの任意の言語で作業したプログラマーには馴染みがあります。しかし、セミコロンはパーサーのためのものであり、人間のためのものではありません。私たちは可能な限りそれらを排除したいと考えました。この目標を達成するために、GoはBCPLからのトリックを借用しています:文を区切るセミコロンは正式な文法に含まれていますが、レキサーによって自動的に挿入され、文の終わりであろう行の終わりに見通しなしで挿入されます。これは実際には非常にうまく機能しますが、波括弧スタイルを強制する効果があります。例えば、関数の開き波括弧は単独の行に現れることはできません。
一部の人々は、レキサーが見通しを行って、波括弧が次の行に存在できるようにすべきだと主張していますが、私たちはそうは思いません。Goコードはgofmt
によって自動的にフォーマットされることを意図しているため、何らかのスタイルを選択する必要があります。そのスタイルは、CやJavaで使用していたものとは異なるかもしれませんが、Goは異なる言語であり、gofmt
のスタイルは他のどのスタイルにも劣らないものです。さらに重要なのは、すべてのGoプログラムに対してプログラム的に義務付けられた単一のフォーマットの利点は、特定のスタイルの認識された欠点を大きく上回るということです。また、Goのスタイルは、Goのインタラクティブな実装が特別なルールなしで標準構文を一行ずつ使用できることを意味します。
なぜガーベジコレクションを行うのか?高コストにならないのか?
システムプログラムにおける管理の最大の源の一つは、割り当てられたオブジェクトのライフタイムを管理することです。Cのような言語では手動で行われるため、プログラマーの時間をかなり消費し、しばしば厄介なバグの原因となります。C++やRustのように支援するメカニズムを提供する言語でも、これらのメカニズムはソフトウェアの設計に大きな影響を与え、しばしば独自のプログラミングオーバーヘッドを追加します。私たちは、プログラマーのオーバーヘッドを排除することが重要だと感じており、近年のガーベジコレクション技術の進歩により、十分に安価で低遅延で実装できる自信を持ちました。これにより、ネットワークシステムにとって実行可能なアプローチとなる可能性があります。
並行プログラミングの多くの困難は、オブジェクトのライフタイム問題に根ざしています。オブジェクトがスレッド間で渡されると、それらが安全に解放されることを保証するのが面倒になります。自動ガーベジコレクションにより、並行コードを書くのがはるかに簡単になります。もちろん、並行環境でガーベジコレクションを実装すること自体が課題ですが、一度それに対処することで、すべてのプログラムでそれを行うよりも、全員にとって助けになります。
最後に、並行性を除けば、ガーベジコレクションはインターフェースを簡素化します。なぜなら、それらの間でメモリがどのように管理されるかを指定する必要がないからです。
これは、リソース管理の問題に新しいアイデアをもたらすRustのような言語での最近の作業が誤っていると言っているわけではありません。私たちはこの作業を奨励し、その進化を楽しみにしています。しかし、Goはガーベジコレクションを通じてオブジェクトのライフタイムに対処することによって、より伝統的なアプローチを取っています。
現在の実装はマークアンドスイープコレクタです。マシンがマルチプロセッサの場合、コレクタはメインプログラムと並行して別のCPUコアで実行されます。近年のコレクタに関する大規模な作業により、特に大きなヒープに対しても、ポーズ時間がしばしばサブミリ秒の範囲に短縮され、ネットワークサーバーにおけるガーベジコレクションに対する主要な異議の一つがほぼ排除されました。アルゴリズムの洗練、オーバーヘッドと遅延のさらなる削減、新しいアプローチの探求が続いています。2018年のISMM基調講演で、GoチームのRick Hudsonがこれまでの進捗を説明し、いくつかの将来のアプローチを提案しています。
パフォーマンスに関しては、Goはプログラマーにメモリのレイアウトと割り当てに対してかなりの制御を提供します。これはガーベジコレクション言語では典型的なものよりもはるかに多いです。注意深いプログラマーは、言語をうまく使用することでガーベジコレクションのオーバーヘッドを劇的に削減できます。具体的な例については、Goプログラムのプロファイリングに関する記事を参照してください。そこにはGoのプロファイリングツールのデモも含まれています。