よくある質問 (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」で入手できます。
プロジェクトの歴史はどうですか?
Robert Griesemer、Rob Pike、Ken Thompsonは、2007年9月21日にホワイトボードで新しい言語の目標をスケッチし始めました。数日以内に目標は何かをする計画に落ち着き、それが何であるかについてかなり明確なアイデアが固まりました。デザインは、無関係な作業と並行してパートタイムで続けられました。2008年1月までに、Kenはアイデアを探るためのコンパイラの作業を開始しました。それはCコードを出力として生成しました。年中までに、言語はフルタイムのプロジェクトとなり、実用的なコンパイラを試すのに十分な安定した状態になりました。2008年5月、Ian Taylorはドラフト仕様を使用してGoのGCCフロントエンドを独自に開発し始めました。Russ Coxは2008年後半に参加し、言語とライブラリをプロトタイプから実用化へと移行させるのを助けました。
Goは2009年11月10日に公開オープンソースプロジェクトとなりました。コミュニティの数え切れない人々がアイデア、議論、コードに貢献してきました。
現在、世界中には何百万ものGoプログラマー(ゴーファーズ)がおり、その数は日々増え続けています。Goの成功は私たちの期待をはるかに超えています。
ゴーファーのマスコットの由来は何ですか?
マスコットとロゴは、Plan 9のウサギであるGlendaもデザインしたRenée Frenchによってデザインされました。ゴーファーに関するブログ記事では、数年前にWFMUのTシャツのデザインで彼女が使用したものからどのように派生したかが説明されています。ロゴとマスコットはCreative Commons Attribution 4.0ライセンスでカバーされています。
ゴーファーには、彼の特徴とそれらを正しく表現する方法を示すモデルシートがあります。このモデルシートは、2016年のGopherconでのRenéeによる講演で初めて公開されました。彼はユニークな特徴を持っています。彼はただの古いゴーファーではなく、Goゴーファーです。
言語の名前はGoですか、それともGolangですか?
言語の名前はGoです。「golang」という愛称は、ウェブサイトが元々golang.orgであったことに由来します。(当時、.devドメインはありませんでした。)しかし、多くの人がgolangという名前を使用しており、ラベルとして便利です。例えば、言語のソーシャルメディアタグは「#golang」です。とにかく、言語の名前は単にGoです。
補足:公式ロゴでは2つの大文字が使われていますが、言語の名前はGoと表記され、GOではありません。
なぜ新しい言語を作成したのですか?
Goは、Googleで私たちが取り組んでいた仕事において、既存の言語と環境への不満から生まれました。プログラミングが難しくなりすぎ、その原因の一部は言語の選択にありました。効率的なコンパイル、効率的な実行、プログラミングの容易さのいずれかを選択しなければならず、これら3つすべてを同じ主流の言語で利用することはできませんでした。プログラマーは、C++や、程度の差はあれJavaではなく、PythonやJavaScriptのような動的型付け言語に移行することで、安全性や効率性よりも容易さを選択していました。
この懸念は私たちだけのものではありませんでした。プログラミング言語の状況が長年静かだった後、Goは、Rust、Elixir、Swiftなど、プログラミング言語開発を再び活発で、ほぼ主流の分野にしたいくつかの新しい言語の最初の1つでした。
Goは、解釈型で動的型付け言語のプログラミングの容易さと、静的型付けでコンパイル型言語の効率性と安全性を組み合わせようとすることで、これらの問題に対処しました。また、ネットワークコンピューティングやマルチコアコンピューティングをサポートすることで、現在のハードウェアによりよく適応することを目指しました。最後に、Goでの作業は「高速」であることが意図されています。つまり、単一のコンピューターで大きな実行可能ファイルをビルドするのに数秒しかかからないはずです。これらの目標を達成するために、私たちは現在の言語からのプログラミングアプローチの一部を再考し、次のような結果をもたらしました。階層的ではなく構成的な型システム。並行処理とガベージコレクションのサポート。依存関係の厳格な仕様。など。これらはライブラリやツールではうまく処理できません。新しい言語が必要とされました。
記事「Go at Google」では、Go言語の設計の背景と動機について、またこのFAQで提示されている多くの回答についてさらに詳細に説明しています。
Goの祖先は何ですか?
Goは主にCファミリー(基本構文)に属し、Pascal/Modula/Oberonファミリー(宣言、パッケージ)からの重要な影響に加え、Tony HoareのCSPに触発されたNewsqueakやLimbo(並行性)などの言語からのアイデアも取り入れています。しかし、全体として新しい言語です。あらゆる点で、プログラマーが何をするか、そしてプログラミング、少なくとも私たちがする種類のプログラミングを、より効果的、つまりより楽しくするにはどうすればよいかを考えて設計されました。
設計の指導原則は何ですか?
Goが設計された当時、JavaとC++は、少なくともGoogleでは、サーバーを記述するために最も一般的に使用される言語でした。これらの言語は、あまりにも多くの管理と繰り返しを必要とすると感じていました。一部のプログラマーは、効率と型安全性を犠牲にして、Pythonのようなより動的で流動的な言語に移行することで反応しました。私たちは、効率、安全性、流動性を単一の言語で持つことが可能であるべきだと感じました。
Goは、両方の意味でタイピングの量を減らそうとします。設計全体を通して、私たちは雑然としたものや複雑さを減らすように努めてきました。前方宣言やヘッダーファイルはありません。すべてが一度だけ正確に宣言されます。初期化は表現力豊かで、自動的で、使いやすいです。構文はきれいでキーワードが少ないです。繰り返し(foo.Foo* myFoo = new(foo.Foo))は、:=宣言および初期化構文を使用した単純な型派生によって削減されます。そして、おそらく最も根本的には、型階層はありません。型はただ「存在する」だけであり、その関係を宣言する必要はありません。これらの簡素化により、Goは生産性を犠牲にすることなく、表現力豊かで理解しやすくなります。
もう1つの重要な原則は、概念を直交させることです。メソッドは任意の型に実装できます。構造体はデータを表し、インターフェースは抽象化を表します。など。直交性により、物事が結合したときに何が起こるかを理解しやすくなります。
使い方
Googleは社内でGoを使用していますか?
はい。GoはGoogle社内で本番環境で広く使用されています。一例として、Chromeバイナリやapt-getパッケージなどの大規模なインストール可能ファイルを提供するGoogleのダウンロードサーバー、dl.google.comがあります。
GoはGoogleで使われている唯一の言語ではありませんが、サイト信頼性エンジニアリング (SRE) や大規模データ処理など、多くの分野で主要な言語となっています。また、Google Cloudを動かすソフトウェアの重要な部分でもあります。
他にどのような企業がGoを使用していますか?
Goの利用は世界中で、特にクラウドコンピューティング分野で成長していますが、それに限定されるものではありません。Goで書かれた主要なクラウドインフラプロジェクトの2つはDockerとKubernetesですが、他にもたくさんあります。
ただし、クラウドだけではありません。go.devウェブサイトの企業リストと、いくつかの成功事例からもわかるように、Go Wikiには、Goを使用している多くの企業をリストアップしたページがあり、定期的に更新されています。
Wikiには、Go言語を使用している企業やプロジェクトの成功事例へのリンクがさらに掲載されています。
GoプログラムはC/C++プログラムとリンクしますか?
CとGoを同じアドレス空間で一緒に使用することは可能ですが、自然な適合ではなく、特別なインターフェースソフトウェアが必要となる場合があります。また、CとGoコードをリンクすると、Goが提供するメモリ安全性とスタック管理プロパティが失われます。問題を解決するためにCライブラリを使用することが絶対に必要となる場合もありますが、そうすることで常に純粋なGoコードには存在しないリスク要素が導入されるため、注意して行ってください。
GoでCを使用する必要がある場合、どのように進めるかはGoコンパイラの実装に依存します。GoogleのGoチームによってサポートされているGoツールチェーンの一部である「標準」コンパイラはgcと呼ばれます。さらに、GCCベースのコンパイラ(gccgo)とLLVMベースのコンパイラ(gollvm)もあり、また、TinyGoのように、言語のサブセットを実装するなど、さまざまな目的に役立つ珍しいもののリストも増え続けています。
GcはCとは異なる呼び出し規約とリンカーを使用するため、Cプログラムから直接呼び出すことはできませんし、その逆もできません。cgoプログラムは、GoコードからCライブラリを安全に呼び出すための「外部関数インターフェース」のメカニズムを提供します。SWIGはこの機能をC++ライブラリに拡張します。
また、cgoとSWIGをgccgoとgollvmと一緒に使用することもできます。これらは従来のABIを使用するため、細心の注意を払えば、これらのコンパイラのコードをGCC/LLVMでコンパイルされたCまたはC++プログラムと直接リンクすることも可能です。ただし、これを安全に行うには、関係するすべての言語の呼び出し規約を理解し、GoからCまたはC++を呼び出す際のスタック制限に注意を払う必要があります。
GoはどのようなIDEをサポートしていますか?
GoプロジェクトにはカスタムIDEは含まれていませんが、言語とライブラリはソースコードの解析を容易にするように設計されています。その結果、ほとんどの有名なエディターとIDEは、直接またはプラグインを通じてGoをうまくサポートしています。
Goチームは、LSPプロトコル用のGo言語サーバーであるgoplsもサポートしています。LSPをサポートするツールは、goplsを使用して言語固有のサポートを統合できます。
Goをうまくサポートする有名なIDEとエディターのリストには、Emacs、Vim、VSCode、Atom、Eclipse、Sublime、IntelliJ(GoLandと呼ばれるカスタムバリアントを通じて)、その他多数が含まれます。お気に入りの環境はGoでプログラミングするのに生産的である可能性が高いです。
GoはGoogleのプロトコルバッファをサポートしていますか?
別のオープンソースプロジェクトが、必要なコンパイラプラグインとライブラリを提供しています。これはgithub.com/golang/protobuf/で入手できます。
デザイン
Goにはランタイムがありますか?
Goには、すべてのGoプログラムの一部である、しばしばランタイムと呼ばれる広範なランタイムライブラリがあります。このライブラリは、ガベージコレクション、並行処理、スタック管理、およびGo言語のその他の重要な機能を実現します。言語の中心的な部分ではありますが、GoのランタイムはCライブラリであるlibcに似ています。
ただし、GoのランタイムはJavaランタイムが提供するような仮想マシンを含まないことを理解することが重要です。Goプログラムは、ネイティブマシンコード(または一部のバリアント実装ではJavaScriptやWebAssembly)に事前にコンパイルされます。したがって、この用語はプログラムが実行される仮想環境を説明するためによく使用されますが、Goでは「ランタイム」という単語は、重要な言語サービスを提供するライブラリに与えられた名前にすぎません。
Unicode識別子についてはどうですか?
Goを設計する際、私たちはそれが過度にASCII中心にならないようにすることを目指しました。これは、識別子の空間を7ビットASCIIの範囲から拡張することを意味しました。Goのルール(識別子文字はUnicodeで定義された文字または数字でなければならない)は理解しやすく、実装も簡単ですが、制限があります。例えば、結合文字は意図的に除外されており、これはデーヴァナーガリーのような一部の言語を除外します。
このルールにはもう一つ不都合な点があります。エクスポートされる識別子は大文字で始まらなければならないため、一部の言語の文字から作成された識別子は、定義上エクスポートできません。今のところ唯一の解決策は、明らかに不満足なX日本語のようなものを使用することです。
言語の初期バージョンから、他のネイティブ言語を使用するプログラマーに対応するために識別子空間をどのように拡張するのが最適かについて、かなりの検討がなされてきました。具体的にどうすべきかは、現在も活発な議論の対象となっており、将来の言語バージョンでは識別子の定義がより自由になるかもしれません。例えば、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、および Recoverの記事を参照してください。また、エラーは値であるというブログ投稿では、エラーが単なる値であるため、Goの全機能をエラー処理に展開できることを示すことで、Goでエラーをクリーンに処理する1つのアプローチを説明しています。
なぜGoにはアサーションがないのですか?
Goにはアサーションがありません。確かに便利ですが、私たちの経験では、プログラマーはそれらを適切なエラー処理と報告について考えることを避けるための杖として使用します。適切なエラー処理とは、致命的ではないエラーが発生した後もサーバーがクラッシュせずに動作し続けることを意味します。適切なエラー報告とは、エラーが直接的かつ要点を得ており、プログラマーが大規模なクラッシュトレースを解釈する手間を省くことを意味します。プログラマーがエラーを見ているコードに精通していない場合、正確なエラーは特に重要です。
これが論争の的となる点であることは理解しています。Go言語とライブラリには、現代の慣行とは異なる点が多数ありますが、それは単に、時には異なるアプローチを試す価値があると感じるからです。
なぜCSPのアイデアに基づいて並行処理を構築するのですか?
並行処理とマルチスレッドプログラミングは、長年にわたって難しさの評判を築いてきました。私たちは、その一因はpthreadsのような複雑な設計にあり、もう一因はミューテックス、条件変数、メモリバリアなどの低レベルの詳細に過度に重点が置かれていることにあると考えています。たとえ裏側にミューテックスなどが存在しても、より高レベルのインターフェースははるかに単純なコードを可能にします。
並行処理に対する高レベルの言語サポートを提供するための最も成功したモデルの1つは、HoareのCommunicating Sequential Processes、つまりCSPに由来しています。OccamとErlangは、CSPから派生した2つのよく知られた言語です。Goの並行処理プリミティブは、別のファミリーツリーの一部から派生しており、その主な貢献は、ファーストクラスオブジェクトとしてのチャネルという強力な概念です。いくつかの以前の言語での経験から、CSPモデルは手続き型言語フレームワークにうまく適合することが示されています。
スレッドではなくゴルーチンを使うのはなぜですか?
ゴルーチンは、並行処理を使いやすくするための一部です。このアイデアは以前から存在しており、独立して実行される関数(コルーチン)を一連のスレッドに多重化するというものです。コルーチンがブロッキングシステムコールを呼び出すなどしてブロックした場合、ランタイムは同じオペレーティングシステムスレッド上の他のコルーチンを別の実行可能なスレッドに自動的に移動するため、ブロックされません。プログラマーにはこれが一切見えないことがポイントです。この結果、私たちがゴルーチンと呼ぶものは非常に安価です。スタックのためのメモリ(わずか数キロバイト)以外のオーバーヘッドはほとんどありません。
スタックを小さくするために、Goのランタイムはサイズ変更可能で制限されたスタックを使用します。新しく作成されたゴルーチンには数キロバイトが与えられますが、これはほとんどの場合十分です。そうでない場合、ランタイムはスタックを格納するためのメモリを自動的に成長(および縮小)させ、多くのゴルーチンが適度な量のメモリで共存できるようにします。CPUオーバーヘッドは、関数呼び出しあたり平均約3つの安価な命令です。同じアドレス空間に数十万のゴルーチンを作成することは実用的です。もしゴルーチンが単なるスレッドであれば、システムリソースははるかに少ない数で枯渇するでしょう。
なぜマップ操作はアトミックであると定義されていないのですか?
長い議論の末、マップの典型的な使用方法では複数のゴルーチンからの安全なアクセスを必要としないこと、そして必要とする場合でも、マップはすでに同期されているより大きなデータ構造または計算の一部である可能性が高いと決定されました。したがって、すべてのマップ操作にミューテックスを要求することは、ほとんどのプログラムを遅くし、安全性もほとんど向上させません。ただし、これは簡単な決定ではありませんでした。なぜなら、制御されないマップアクセスがプログラムをクラッシュさせる可能性があることを意味するからです。
この言語はアトミックなマップ更新を排除していません。信頼できないプログラムをホストする場合など、必要な場合は、実装がマップアクセスをインターロックできます。
マップアクセスは、更新が行われている場合にのみ安全ではありません。すべてのゴルーチンが読み取りのみを行っている場合(マップ内の要素の検索、for rangeループを使用したイテレーションを含む)、要素への割り当てや削除によってマップを変更しない限り、同期なしでマップに同時にアクセスしても安全です。
正しいマップの使用を助けるために、言語のいくつかの実装には、並行実行によってマップが安全でない方法で変更された場合に、実行時に自動的に報告する特別なチェックが含まれています。また、syncライブラリには、静的キャッシュなどの特定の使用パターンにうまく機能するsync.Mapという型がありますが、組み込みのマップ型の一般的な代替品としては適していません。
私の言語変更を受け入れてもらえますか?
人々はしばしば言語の改善を提案します。メーリングリストにはそのような議論の豊富な履歴が含まれていますが、これらの変更のうち承認されたものはごくわずかです。
Goはオープンソースプロジェクトですが、言語とライブラリは、既存のプログラムを壊す変更(少なくともソースコードレベルで。プログラムは最新の状態を保つために時々再コンパイルする必要があるかもしれません)を防ぐ互換性保証によって保護されています。あなたの提案がGo 1仕様に違反する場合、そのメリットにかかわらず、私たちはそのアイデアを検討することさえできません。Goの将来のメジャーリリースは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は異なるアプローチをとります。
プログラマーに2つの型が関連していることを事前に宣言させる代わりに、Goでは型はそのメソッドのサブセットを指定する任意のインターフェースを自動的に満たします。このアプローチは、管理の手間を減らすだけでなく、実際の利点があります。型は、従来の多重継承の複雑さなしに、一度に多くのインターフェースを満たすことができます。インターフェースは非常に軽量にすることができます。1つまたは0個のメソッドを持つインターフェースでさえ、有用な概念を表現できます。新しいアイデアが生まれた場合やテストのために、元の型をアノテーションすることなく、後からインターフェースを追加できます。型とインターフェースの間に明示的な関係がないため、管理したり議論したりする型階層はありません。
これらのアイデアを使って、型安全なUnixパイプに似たものを作成することも可能です。例えば、fmt.Fprintfがファイルだけでなく任意の出力への書式付き印刷を可能にする方法、またはbufioパッケージがファイルI/Oから完全に独立できる方法、またはimageパッケージが圧縮画像ファイルを生成する方法をご覧ください。これらのアイデアはすべて、単一のメソッド(Write)を表す単一のインターフェース(io.Writer)に由来しています。そして、それはまだほんの一部にすぎません。Goのインターフェースは、プログラムがどのように構造化されるかに深い影響を与えます。
慣れるには時間がかかりますが、この暗黙的な型依存のスタイルは、Goの最も生産的な点の1つです。
なぜlenは関数であってメソッドではないのですか?
この問題については議論しましたが、lenとその仲間を関数として実装することは実際には問題なく、基本型のインターフェース(Goの型という意味で)に関する問題を複雑にしないと判断しました。
なぜGoはメソッドと演算子のオーバーロードをサポートしないのですか?
メソッドのディスパッチは、型の一致も必要としない場合に簡素化されます。他の言語での経験から、同じ名前で異なるシグネチャを持つさまざまなメソッドを持つことは、時折役立つが、実際には混乱を招き、脆くなる可能性があることがわかりました。名前のみで一致させ、型の整合性を要求することは、Goの型システムにおける主要な簡素化の決定でした。
演算子のオーバーロードに関しては、絶対的な要件というよりも、むしろ利便性の問題であるように思われます。繰り返しになりますが、それがない方が物事は単純になります。
なぜGoには「implements」宣言がないのですか?
Goの型は、そのインターフェースのメソッドを実装することでインターフェースを実装します。それ以上でもそれ以下でもありません。このプロパティにより、既存のコードを変更することなくインターフェースを定義して使用できます。これにより、関心の分離とコードの再利用を促進し、コードが開発されるにつれて現れるパターンに基づいて構築しやすくなる、ある種の構造的型付けが可能になります。インターフェースのセマンティクスは、Goの機敏で軽量な感覚の主な理由の1つです。
詳細については、型継承に関する質問を参照してください。
型がインターフェースを満たすことをどのように保証できますか?
コンパイラに、型TがインターフェースIを実装していることを確認させるには、必要に応じてTのゼロ値またはTへのポインタを使用して代入を試みます。
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の引数を昇格させません。それはプログラマーの責任であり、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では、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)を持つ可能性があるため、インターフェースの動的値としても知られています。
インターフェース値は、VとTの両方が設定されていない場合にのみnilになります(T=nil、Vは設定されていない)。特に、nilインターフェースは常にnil型を保持します。型*intのnilポインターをインターフェース値内に格納すると、ポインターの値に関係なく内部型は*intになります(T=*int、V=nil)。したがって、そのようなインターフェース値は、ポインター値Vがnilである場合でも、非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を返します。したがって、戻り値は(T=*MyError, V=nil)を保持するerrorインターフェース値となります。これは、呼び出し元が返されたエラーをnilと比較した場合、何も悪いことが起こらなかったとしても、常にエラーがあったように見えることを意味します。呼び出し元に適切なnilのerrorを返すには、関数は明示的なnilを返す必要があります。
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
エラーを返す関数は、エラーが正しく作成されることを保証するために、具体的な型(*MyErrorなど)ではなく、そのシグネチャで常にerror型を使用することをお勧めします(上記のように)。例として、os.Openは、nilでない場合でも、常に具体的な型*os.PathErrorであるにもかかわらず、errorを返します。
ここで説明したような状況は、インターフェースが使用されるたびに発生する可能性があります。インターフェースに具体的な値が格納されている場合、インターフェースはnilではないということを覚えておいてください。詳細については、The Laws of Reflectionを参照してください。
なぜゼロサイズ型は奇妙な動作をするのですか?
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実装では、1ではなく2と出力されます。
Cのようにタグなし共用体がないのはなぜですか?
タグなし共用体はGoのメモリ安全性保証に違反します。
なぜGoにはバリアント型がないのですか?
代数的型とも呼ばれるバリアント型は、値が他の型のセットの1つ、ただしそれらの型のみを取ることができることを指定する方法を提供します。システムプログラミングにおける一般的な例では、エラーが、たとえばネットワークエラー、セキュリティエラー、またはアプリケーションエラーであり、呼び出し元がエラーの型を調べることによって問題の原因を区別できるように指定します。もう1つの例は、各ノードが異なる型である構文ツリーです。宣言、ステートメント、代入などです。
Goにバリアント型を追加することを検討しましたが、議論の結果、インターフェースと混乱するような方法で重複するため、含めないことにしました。バリアント型の要素自体がインターフェースであった場合、どうなるでしょうか?
また、バリアント型が扱う一部はすでに言語によってカバーされています。エラーの例は、エラーを保持するためのインターフェース値と、ケースを区別するための型スイッチを使用して簡単に表現できます。構文ツリーの例も可能ですが、それほどエレガントではありません。
なぜGoには共変な結果型がないのですか?
共変な結果型とは、次のようなインターフェースを意味します。
type Copyable interface {
Copy() interface{}
}
メソッドによって満たされることになります。
func (v Value) Copy() Value
なぜならValueは空のインターフェースを実装しているからです。Goではメソッドの型は完全に一致しなければならないため、ValueはCopyableを実装しません。Goは、型の機能(そのメソッド)と型の実装の概念を分離します。2つのメソッドが異なる型を返す場合、それらは同じことをしていません。共変な結果型を求めるプログラマーは、しばしばインターフェースを通じて型階層を表現しようとします。Goでは、インターフェースと実装の間を明確に分離する方が自然です。
値
Goが暗黙的な数値変換を提供しないのはなぜですか?
C言語における数値型間の自動変換の便利さは、それが引き起こす混乱によって相殺されます。式はいつ符号なしになるのか?値の大きさはどのくらいか?オーバーフローするか?結果は実行されるマシンに依存せず移植可能か?これはコンパイラも複雑にします。Cの「通常の算術変換」は実装が容易ではなく、アーキテクチャ間で一貫性がありません。移植性の理由から、コード中のいくつかの明示的な変換を犠牲にして、物事を明確かつ単純にすることにしました。ただし、Goにおける定数の定義(符号性やサイズのアノテーションに左右されない任意精度の値)は、この問題をかなり改善します。
関連する詳細は、Cとは異なり、たとえintが64ビット型であっても、intとint64は別々の型であるということです。int型は汎用的です。整数のビット数に関心がある場合は、Goでは明示的に指定することをお勧めします。
Goにおける定数はどのように機能しますか?
Goは異なる数値型の変数間の変換には厳格ですが、言語内の定数ははるかに柔軟です。23、3.14159、math.Piのようなリテラル定数は、任意精度でオーバーフローやアンダーフローのない理想的な数値空間を占めます。たとえば、math.Piの値はソースコードで63桁の小数点以下まで指定されており、その値を含む定数式はfloat64が保持できる精度を超えて精度を保持します。定数または定数式が変数(プログラム内のメモリ位置)に割り当てられるときにのみ、通常の浮動小数点プロパティと精度を持つ「コンピューター」数値になります。
また、定数は型付けされた値ではなく単なる数値であるため、Goの定数は変数よりも自由に使うことができ、厳格な変換ルールをめぐるいくつかの不便さを和らげます。次のような式を書くことができます。
sqrt2 := math.Sqrt(2)
コンパイラからの不満なく、理想的な数値2はmath.Sqrtの呼び出しのためにfloat64に安全かつ正確に変換できるからです。
「Constants」というブログ記事で、このトピックがより詳細に探求されています。
なぜマップが組み込まれているのですか?
文字列が組み込まれているのと同じ理由です。それらは非常に強力で重要なデータ構造であり、構文サポートを備えた優れた実装を提供することで、プログラミングがより快適になります。Goのマップの実装は、ほとんどの用途に役立つほど強力であると私たちは信じています。特定のアプリケーションがカスタム実装から恩恵を受けることができる場合、それを記述することは可能ですが、構文的にはそれほど便利ではありません。これは合理的なトレードオフであると思われます。
なぜマップはスライスをキーとして許可しないのですか?
マップルックアップには等値演算子が必要ですが、スライスはそれを実装していません。スライスが等値性を実装しないのは、そのような型では等値性が明確に定義されていないためです。浅い比較と深い比較、ポインタ比較と値比較、再帰型への対処方法など、複数の考慮事項があります。この問題は再検討するかもしれませんが、スライスの等値性が何を意味すべきかについて明確なアイデアがないため、当面は省略する方が簡単でした。
構造体と配列には等値性が定義されているため、マップキーとして使用できます。
マップ、スライス、チャネルが参照なのに、なぜ配列は値なのですか?
このトピックには多くの歴史があります。初期の頃、マップとチャネルは構文上ポインタであり、非ポインタインスタンスを宣言または使用することは不可能でした。また、配列がどのように機能すべきかにも苦慮しました。最終的に、ポインタと値の厳密な分離が言語を使いにくくしていると判断しました。これらの型を関連する共有データ構造への参照として機能するように変更することで、これらの問題は解決されました。この変更により、言語にいくらか残念な複雑さが加わりましたが、使いやすさに大きな影響を与えました。導入後、Goはより生産的で快適な言語になりました。
コードを書く
ライブラリはどのように文書化されていますか?
コマンドラインからドキュメントにアクセスするには、goツールに、宣言、ファイル、パッケージなどのドキュメントに対するテキストインターフェースを提供するdocサブコマンドがあります。
グローバルパッケージ発見ページpkg.go.dev/pkg/は、Web上のGoソースコードからパッケージドキュメントを抽出し、宣言や関連要素へのリンクを含むHTMLとして提供するサーバーを実行します。これは、既存のGoライブラリについて学ぶ最も簡単な方法です。
プロジェクトの初期には、ローカルマシンのファイルのドキュメントを抽出できる類似のプログラムgodocがありました。pkg.go.dev/pkg/は本質的にその子孫です。もう1つの子孫はpkgsiteコマンドで、godocと同様にローカルで実行できますが、go docで表示される結果にはまだ統合されていません。
Goプログラミングのスタイルガイドはありますか?
明示的なスタイルガイドはありませんが、確かに認識できる「Goスタイル」は存在します。
Goには、命名、レイアウト、ファイル構成に関する決定を導くための慣習が確立されています。Effective Goというドキュメントには、これらのトピックに関するアドバイスが記載されています。より直接的には、gofmtプログラムは、レイアウトルールを強制することを目的としたプリティプリンターです。これは、解釈を許す通常のすべきこととすべきでないことの要約に取って代わるものです。リポジトリ内のすべての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)を含む他のポートでの発信トラフィックをブロックします。HTTPSの代わりにHTTPを使用する場合、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/
プライベートモジュールを使用しているが、依存関係にパブリックモジュールプロキシを使用している場合、GOPRIVATEを設定する必要があるかもしれません。詳細と追加設定については、プライベートモジュールを参照してください。
「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の値レシーバーが珍しいのです。
2番目は効率性です。レシーバーが大きい場合、たとえば大きなstructの場合、ポインタレシーバーを使用する方が安価な場合があります。
次は一貫性です。型のいくつかのメソッドがポインタレシーバーを持たなければならない場合、残りのメソッドも同様に持つべきです。これにより、型がどのように使用されるかに関係なく、メソッドセットが一貫します。詳細については、メソッドセットのセクションを参照してください。
基本型、スライス、小さなstructなどの型の場合、値レシーバーは非常に安価であるため、メソッドのセマンティクスがポインタを必要としない限り、値レシーバーは効率的で明確です。
newとmakeの違いは何ですか?
簡単に言うと、newはメモリを割り当て、makeはスライス、マップ、チャネルの型を初期化します。
詳細については、Effective Goの関連セクションを参照してください。
64ビットマシンでのintのサイズはどれくらいですか?
intとuintのサイズは実装固有ですが、特定のプラットフォーム上では互いに同じです。移植性のために、特定のサイズの変数に依存するコードは、int64のように明示的にサイズが指定された型を使用する必要があります。32ビットマシンではコンパイラはデフォルトで32ビット整数を使用しますが、64ビットマシンでは整数は64ビットを持ちます。(歴史的には、常にそうであったわけではありません。)
一方、浮動小数点スカラーと複素数型は常にサイズが指定されています(floatやcomplexの基本型はありません)。これは、プログラマーが浮動小数点数を使用する際に精度を認識すべきだからです。(型なしの)浮動小数点定数にデフォルトで使われる型はfloat64です。したがって、foo := 3.0はfloat64型の変数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メモリモデルドキュメントで説明されています。
低レベルの同期およびアトミックプリミティブは、syncおよびsync/atomicパッケージで利用できます。これらのパッケージは、参照カウントのインクリメントや小規模な相互排他の保証などの単純なタスクに適しています。
同時実行サーバー間の調整のようなより高レベルの操作には、より高レベルの技術がより良いプログラムにつながることがあり、Goはゴルーチンとチャネルを通じてこのアプローチをサポートしています。たとえば、一度に1つのゴルーチンだけが特定のデータに責任を持つようにプログラムを構造化できます。このアプローチは、元のGoの諺に要約されています。
メモリ共有による通信はしない。代わりに、通信によるメモリ共有をする。
この概念の詳細については、Share Memory By Communicatingコードウォークと、それに関連する記事を参照してください。
大規模な並行プログラムは、これらのツールキットの両方から借用する可能性が高いです。
なぜ私のプログラムはCPUを増やしても速くならないのですか?
プログラムがより多くのCPUで高速に実行されるかどうかは、それが解決している問題に依存します。Go言語は、ゴルーチンやチャネルなどの並行処理プリミティブを提供しますが、並行処理は、基礎となる問題が本質的に並列である場合にのみ並列処理を可能にします。本質的にシーケンシャルな問題は、より多くのCPUを追加しても高速化できませんが、並列に実行できる部分に分割できる問題は、劇的に高速化される場合があります。
CPUを増やすと、プログラムが遅くなることがあります。実用的に言えば、有用な計算を行うよりも同期や通信に多くの時間を費やすプログラムは、複数のOSスレッドを使用するとパフォーマンスが低下する可能性があります。これは、スレッド間でデータを渡す際にコンテキストスイッチが発生し、それがかなりのコストを伴い、CPUが増えるにつれてそのコストが増大する可能性があるためです。例えば、Go仕様の素数篩の例は、多くのゴルーチンを起動しますが、実質的な並列性はありません。スレッド(CPU)の数を増やすと、高速化するよりも遅くなる可能性の方が高いです。
このトピックに関する詳細については、「Concurrency is not Parallelism」という講演を参照してください。
CPUの数をどのように制御できますか?
ゴルーチンが同時に実行できるCPUの数は、GOMAXPROCSシェル環境変数によって制御され、そのデフォルト値は利用可能なCPUコアの数です。したがって、並列実行の可能性があるプログラムは、マルチCPUマシンではデフォルトでそれを達成するはずです。使用する並列CPUの数を変更するには、環境変数を設定するか、ランタイムパッケージの同名の関数を使用して、ランタイムサポートを異なる数のスレッドを利用するように構成します。これを1に設定すると、真の並列処理の可能性が排除され、独立したゴルーチンが交互に実行することを強制されます。
ランタイムは、複数の未処理のI/O要求を処理するために、GOMAXPROCSの値よりも多くのスレッドを割り当てることができます。GOMAXPROCSは、実際に一度に実行できるゴルーチンの数にのみ影響します。システムコールで任意に多くのゴルーチンがブロックされる可能性があります。
Goのゴルーチンスケジューラは、ゴルーチンとスレッドのバランスをうまく取り、同じスレッド上の他のゴルーチンが飢餓状態にならないように、ゴルーチンの実行をプリエンプションすることさえできます。しかし、それは完璧ではありません。パフォーマンスの問題が見られる場合、アプリケーションごとにGOMAXPROCSを設定することが役立つかもしれません。
なぜゴルーチンIDがないのですか?
ゴルーチンには名前がありません。それらは単なる匿名のワーカーです。プログラマーには一意の識別子、名前、データ構造を公開しません。一部の人々はこのことに驚き、go文が後でゴルーチンにアクセスして制御するために使用できる何らかの項目を返すことを期待します。
ゴルーチンが匿名である根本的な理由は、並行コードをプログラミングする際にGo言語の全機能を利用できるようにするためです。対照的に、スレッドやゴルーチンに名前を付けるときに発展する使用パターンは、それらを使用するライブラリができることを制限する可能性があります。
困難の例を挙げます。ゴルーチンに名前を付けてその周りにモデルを構築すると、それが特別になり、すべての計算をそのゴルーチンに関連付けようとする誘惑に駆られ、複数の、おそらく共有されるゴルーチンを処理に使用する可能性を無視してしまいます。net/httpパッケージがリクエストごとの状態をゴルーチンに関連付けた場合、クライアントはリクエストを処理する際に複数のゴルーチンを使用できなくなります。
さらに、グラフィックシステムのように、すべての処理が「メインスレッド」で発生することを要求するライブラリの経験から、並行言語で展開された場合にこのアプローチがいかに不格好で制限的であるかが示されています。特別なスレッドやゴルーチンの存在自体が、プログラマーに、誤って間違ったスレッドで操作することによって引き起こされるクラッシュやその他の問題を回避するためにプログラムを歪めることを強制します。
特定のゴルーチンが本当に特別なケースでは、言語はチャネルなどの機能を提供しており、それらと柔軟にやり取りするために使用できます。
関数とメソッド
なぜTと*Tは異なるメソッドセットを持つのですか?
Goの仕様が述べているように、型Tのメソッドセットは、レシーバー型がTのすべてのメソッドで構成され、対応するポインタ型*Tのメソッドセットは、レシーバーが*TまたはTのすべてのメソッドで構成されます。つまり、*TのメソッドセットにはTのメソッドセットが含まれますが、逆は違います。
この区別が生じるのは、インターフェース値がポインター*Tを含んでいる場合、メソッド呼び出しはポインターを逆参照することで値を取得できますが、インターフェース値が値Tを含んでいる場合、メソッド呼び出しがポインターを安全に取得する方法がないためです。(そうすると、メソッドがインターフェース内の値の内容を変更できてしまい、これは言語仕様で許可されていません。)
コンパイラがメソッドに渡す値のアドレスを取得できる場合でも、メソッドが値を変更した場合、その変更は呼び出し元に失われます。
例として、以下のコードが有効だった場合
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
それは標準入力をbuf自体ではなく、bufの「コピー」にコピーすることになります。これはほとんどの場合、望ましい動作ではないため、言語によって許可されていません。
ゴルーチンとして実行されるクロージャはどうなりますか?
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の値をプリントしますが、ゴルーチンが起動されてから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 1.22では実際に各イテレーションで新しい変数が作成され、この問題は解消されます。
制御フロー
Goには?:演算子がないのはなぜですか?
Goには三項演算子はありません。同じ結果を得るには、以下の方法を使用できます。
if expr {
n = trueVal
} else {
n = falseVal
}
?:がGoに存在しない理由は、言語設計者がこの演算子が不透明で複雑な式を作成するためにあまりにも頻繁に使用されるのを見てきたからです。if-else形式は、より長くはなりますが、議論の余地なくより明確です。言語には1つの条件付き制御フロー構造があれば十分です。
型パラメータ
なぜGoには型パラメータがあるのですか?
型パラメーターを使用すると、ジェネリックプログラミングとして知られるものが可能になります。これは、関数やデータ構造が、後でそれらの関数やデータ構造が使用されるときに指定される型で定義されるものです。たとえば、これにより、順序付け可能な任意の型の2つの値の最小値を返す関数を、可能なすべての型に対して個別のバージョンを作成することなく記述することができます。例を含むより詳細な説明については、ブログ記事Why Generics?を参照してください。
Goではジェネリクスはどのように実装されていますか?
コンパイラは、各インスタンス化を個別にコンパイルするか、類似のインスタンス化を単一の実装としてコンパイルするかを選択できます。単一の実装アプローチは、インターフェースパラメーターを持つ関数に似ています。異なるコンパイラは、異なるケースで異なる選択をします。標準のGoコンパイラは通常、同じシェイプを持つすべての型引数に対して単一のインスタンス化を生成します。ここで、シェイプは、その型が持つポインタのサイズや場所などのプロパティによって決定されます。将来のリリースでは、コンパイル時間、実行時効率、コードサイズの間のトレードオフを実験する可能性があります。
Goのジェネリクスは他の言語のジェネリクスと比較してどうですか?
すべての言語の基本的な機能は似ています。後で指定される型を使用して型や関数を記述することができます。しかし、いくつかの違いがあります。
-
Java
Javaでは、コンパイラはコンパイル時にジェネリック型をチェックしますが、実行時には型を削除します。これは型消去 (type erasure)として知られています。たとえば、コンパイル時に
List<Integer>として知られるJavaの型は、実行時には非ジェネリック型Listになります。これは、たとえば、Java形式の型リフレクションを使用する場合、List<Integer>型の値とList<Float>型の値を区別できないことを意味します。Goでは、ジェネリック型のリフレクション情報には、完全なコンパイル時型情報が含まれます。Javaは、ジェネリックの共変性 (covariance) と反変性 (contravariance) を実装するために、
List<? extends Number>やList<? super Number>のような型ワイルドカードを使用します。Goにはこれらの概念がないため、Goのジェネリック型ははるかに単純です。 -
C++
伝統的にC++のテンプレートは型引数に何の制約も課しませんでしたが、C++20ではコンセプト (concepts)を介してオプションの制約をサポートしています。Goでは、すべての型パラメーターに対して制約が必須です。C++20のコンセプトは、型引数でコンパイルする必要がある小さなコードフラグメントとして表現されます。Goの制約は、許可されるすべての型引数のセットを定義するインターフェース型です。
C++はテンプレートメタプログラミングをサポートしていますが、Goはサポートしていません。実際には、すべてのC++コンパイラは各テンプレートがインスタンス化される時点でコンパイルします。上記で述べたように、Goは異なるインスタンス化に対して異なるアプローチを使用できますし、実際に使用しています。
-
Rust
Rustの制約はトレイト境界 (trait bounds) として知られています。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の重要な設計決定の1つは、型情報なしで解析が可能であることですが、ジェネリクスに山括弧を使用するとこれは不可能に見えます。
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つの選択肢があります。
-
リンク時に、可能なすべての動的インターフェースチェックのリストを作成し、それらを満たすがコンパイルされたメソッドが欠落している型を探し、コンパイラを再呼び出ししてそれらのメソッドを追加します。
これにより、リンク後に停止し、いくつかのコンパイルを繰り返す必要があるため、ビルドが著しく遅くなります。特にインクリメンタルビルドが遅くなります。さらに悪いことに、新しくコンパイルされたメソッドコード自体が新しい動的インターフェースチェックを持つ可能性があり、そのプロセスを繰り返す必要があります。プロセスが完了しない例を構築することも可能です。
-
何らかのJITを実装し、必要なメソッドコードを実行時にコンパイルします。
Goは、純粋に先行コンパイルされることによるシンプルさと予測可能なパフォーマンスから大きな恩恵を受けています。1つの言語機能を実装するためだけにJITの複雑さを引き受けることには消極的です。
-
型パラメーターに対する可能なすべての言語操作のための関数テーブルを使用する各ジェネリックメソッドの遅いフォールバックを生成し、そのフォールバック実装を動的テストに使用するように手配します。
このアプローチでは、予期しない型でパラメーター化されたジェネリックメソッドは、コンパイル時に観測された型でパラメーター化された同じメソッドよりもはるかに遅くなります。これにより、パフォーマンスの予測可能性が大幅に低下します。
-
ジェネリックメソッドはインターフェースを満たすために一切使用できないと定義します。
インターフェースは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関数を見つけ、テストバイナリをビルドして実行します。
詳細については、How to Write Go Codeドキュメント、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_がないのですか?
標準ライブラリの目的は、ランタイムライブラリをサポートし、オペレーティングシステムに接続し、多くのGoプログラムが必要とする主要な機能(フォーマットされたI/Oやネットワークなど)を提供することです。また、暗号化や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で再帰下降パーサーを使用して書かれており、カスタムローダー(これもGoで書かれていますが、Plan 9ローダーに基づいています)を使用してELF/Mach-O/PEバイナリを生成します。
Gccgoコンパイラは、C++で記述された再帰下降パーサーと標準GCCバックエンドを組み合わせたフロントエンドです。実験的なLLVMバックエンドも同じフロントエンドを使用しています。
プロジェクトの開始当初、gcにLLVMを使用することを検討しましたが、パフォーマンス目標を満たすには大きすぎて遅すぎると判断しました。さらに重要なのは、LLVMから始めると、Goが必要とするが標準Cのセットアップには含まれていないABIと関連する変更、例えばスタック管理などを導入するのが難しくなっただろうということです。
Goは、Goコンパイラを実装するのに優れた言語であることが判明しましたが、それが当初の目的ではありませんでした。最初から自己ホスティングでなかったため、Goの設計は、その本来のユースケースであるネットワークサーバーに集中することができました。もしGoが早い段階で自己コンパイルすべきだと決定していたら、コンパイラ構築により特化した言語になっていたかもしれませんが、それは価値のある目標ですが、私たちが当初持っていたものではありません。
gcは独自のインプリメンテーションを持っていますが、ネイティブなレクサーとパーサーはgo/parserパッケージで利用でき、ネイティブな型チェッカーもあります。gcコンパイラはこれらのライブラリの派生版を使用しています。
ランタイムサポートはどのように実装されていますか?
再びブートストラップの問題により、ランタイムコードは当初ほとんどC(ごくわずかなアセンブラを含む)で書かれていましたが、その後Goに翻訳されました(一部のアセンブラ部分を除く)。Gccgoのランタイムサポートはglibcを使用しています。gccgoコンパイラは、最近のgoldリンカーの変更によってサポートされているセグメントスタックという手法を使用してゴルーチンを実装しています。Gollvmも同様に、対応するLLVMインフラストラクチャ上に構築されています。
なぜ私の簡単なプログラムはこんなに大きなバイナリになるのですか?
gcツールチェーンのリンカーは、デフォルトで静的リンクされたバイナリを作成します。したがって、すべてのGoバイナリには、動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要なランタイム型情報とともに、Goランタイムが含まれます。
Linuxでgccを使って静的にコンパイル・リンクされた簡単なCの「hello, world」プログラムは約750kBで、printfの実装が含まれます。fmt.Printfを使った同等のGoプログラムは数メガバイトですが、これにはより強力なランタイムサポートと型情報、デバッグ情報が含まれています。
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プログラマは、Goソースファイルを自動的に正しいインポートを持つように書き換え、実際には未使用のインポートの問題を解消するgoimportsというツールを使用しています。このプログラムは、ほとんどのエディタやIDEに簡単に接続でき、Goソースファイルが記述されたときに自動的に実行されます。この機能は、上記で説明されているように、goplsにも組み込まれています。
なぜ私のウイルススキャンソフトウェアは、私のGoディストリビューションまたはコンパイル済みバイナリが感染していると判断するのですか?
これはよくあることで、特にWindowsマシンでは、ほとんどの場合誤検知です。商用のウイルススキャンプログラムは、Goバイナリの構造に混乱することがよくあります。彼らは他の言語からコンパイルされたものほど頻繁にGoバイナリを見ることがありません。
Goディストリビューションをインストールしたばかりで、システムが感染していると報告された場合、それは間違いなく誤りです。徹底的に確認するには、ダウンロードページのチェックサムと比較してダウンロードを検証できます。
いずれにせよ、報告が誤りであると思われる場合は、ウイルススキャンの供給元にバグを報告してください。もしかしたら、いずれウイルススキャナはGoプログラムを理解できるようになるかもしれません。
パフォーマンス
なぜGoはベンチマークXでパフォーマンスが悪いのですか?
Goの設計目標の1つは、同等のプログラムでCのパフォーマンスに近づくことですが、golang.org/x/exp/shootoutのいくつかを含む、一部のベンチマークではかなり悪い結果を出します。最も遅いものは、Goで同等のパフォーマンスのバージョンが利用できないライブラリに依存しています。たとえば、pidigits.goは多倍長演算パッケージに依存しており、CバージョンはGoとは異なりGMP(最適化されたアセンブラで書かれている)を使用しています。正規表現に依存するベンチマーク(たとえば、regex-dna.go)は、本質的にGoのネイティブregexpパッケージと、PCREのような成熟した高度に最適化された正規表現ライブラリを比較していることになります。
ベンチマークゲームは、広範なチューニングによって勝ち取られ、ほとんどのベンチマークのGoバージョンには注意が必要です。真に同等のCとGoのプログラムを測定すれば(reverse-complement.goはその一例です)、このスイートが示すよりも、2つの言語は生のパフォーマンスでずっと近いことがわかります。
それでも、改善の余地はあります。コンパイラは優れていますが、さらに改善できますし、多くのライブラリには大規模なパフォーマンス改善が必要です。また、ガベージコレクタはまだ十分高速ではありません。(たとえそうであっても、不必要なガベージを生成しないように注意するだけで大きな効果が得られます。)
いずれにせよ、Goはしばしば非常に競争力があります。言語とツールが発展するにつれて、多くのプログラムのパフォーマンスが大幅に向上しました。Goプログラムのプロファイリングに関するブログ記事で、有益な例をご覧ください。かなり古い記事ですが、今でも役立つ情報が含まれています。
Cからの変更点
なぜCと構文がこんなに違うのですか?
宣言の構文を除けば、大きな違いはなく、2つの願望から生じています。1つは、構文が軽快であるべきで、必須のキーワード、繰り返し、難解なものが多すぎないようにすること。2つ目は、言語が分析しやすく、シンボルテーブルなしで解析できるように設計されていることです。これにより、デバッガー、依存関係アナライザー、自動ドキュメント抽出ツール、IDEプラグインなどのツールを構築するのがはるかに容易になります。Cとその派生言語はこの点で特に難しいことで知られています。
なぜ宣言が逆なのですか?
Cに慣れている場合にのみ、逆だと感じられるでしょう。Cでは、変数はその型を示す式のように宣言されるという考え方があり、これは良いアイデアですが、型と式の文法はあまりうまく混ざり合わず、結果として混乱を招く可能性があります。関数ポインタを考えてみてください。Goは式の構文と型の構文をほとんど分離しており、これにより物事が簡素化されます(ポインタにプレフィックス*を使用するのは例外です)。Cでは、宣言
int* a, b;
はaをポインタとして宣言しますが、bは宣言しません。Goでは
var a, b *int
は両方をポインタとして宣言します。これはより明確で規則的です。また、:=の短縮宣言形式は、完全な変数宣言が:=と同じ順序で記述されるべきだと主張しており、そのため
var a uint64 = 1
は次と同じ効果を持ちます。
a := uint64(1)
型のために式の文法とは異なる明確な文法があることで、解析も簡素化されます。funcやchanのようなキーワードは物事を明確に保ちます。
詳細については、Goの宣言構文に関する記事を参照してください。
なぜポインタ演算がないのですか?
安全性のためです。ポインタ演算がなければ、不正なアドレスを誤って成功させるような言語を作成することは不可能になります。コンパイラとハードウェア技術は、配列インデックスを使用するループがポインタ演算を使用するループと同じくらい効率的になる点まで進歩しました。また、ポインタ演算がないことで、ガベージコレクタの実装を簡素化できます。
なぜ++と--は文であり、式ではないのですか?そしてなぜ前置ではなく後置なのですか?
ポインタ演算がない場合、前置および後置インクリメント演算子の利便性は低下します。これらを式階層から完全に削除することで、式の構文が簡素化され、++と--の評価順序に関する厄介な問題(f(i++)やp[i] = q[++i]を考えてみてください)も排除されます。この簡素化は重要です。後置か前置かについては、どちらでも問題ありませんが、後置の方が伝統的です。前置の主張はSTLとともに生じました。皮肉なことに、その言語の名前には後置インクリメントが含まれています。
なぜ波括弧はあるのにセミコロンはないのですか?そしてなぜ開始波括弧を次の行に置けないのですか?
Goは、Cファミリーの言語を扱ったことがあるプログラマにはおなじみの構文である、ステートメントのグループ化のために波括弧を使用します。しかし、セミコロンはパーサーのためのものであり、人間のためではないため、可能な限りそれらを排除したかったのです。この目標を達成するために、GoはBCPLのトリックを借用しています。ステートメントを区切るセミコロンは形式文法にはありますが、ステートメントの終わりになる可能性のある行の最後に、先読みなしでレクサーによって自動的に挿入されます。これは実際には非常にうまく機能しますが、波括弧のスタイルを強制するという効果があります。たとえば、関数の開始波括弧はそれ自体が単独の行に現れることはできません。
一部の人々は、レクサーが次の行に波括弧を置くことを許可するために先読みを行うべきだと主張してきました。私たちは同意しません。gofmtによってGoコードが自動的にフォーマットされることを意図しているため、_何らかの_スタイルを選択する必要があります。そのスタイルはCやJavaで慣れ親しんだものとは異なるかもしれませんが、Goは異なる言語であり、gofmtのスタイルは他のどのスタイルと同じくらい良いものです。さらに重要なこととして、すべてのGoプログラムに単一の、プログラムによって強制されるフォーマットがあることの利点は、特定のスタイルの欠点と認識されるものをはるかに上回ります。また、Goのスタイルは、Goの対話型実装が特別なルールなしで、一度に1行ずつ標準構文を使用できることを意味することにも注意してください。
なぜガベージコレクションをするのですか?費用が高すぎるのではありませんか?
システムプログラムにおける簿記の最大の原因の1つは、割り当てられたオブジェクトの寿命管理です。Cのような手動で行う言語では、プログラマの時間を大量に消費し、しばしば悪質なバグの原因となります。C++やRustのように支援メカニズムを提供する言語でも、それらのメカニズムはソフトウェアの設計に大きな影響を与え、しばしばそれ自体がプログラミングのオーバーヘッドを追加します。私たちは、そのようなプログラマのオーバーヘッドを排除することが重要だと感じ、過去数年間のガベージコレクション技術の進歩により、ネットワーク化されたシステムにとって実行可能なアプローチとなるほど安価に、そして十分低いレイテンシで実装できるという自信を得ました。
並行プログラミングの困難さの多くは、オブジェクトの寿命問題に根ざしています。オブジェクトがスレッド間で渡されると、それらが安全に解放されることを保証するのが煩雑になります。自動ガベージコレクションは、並行コードの記述をはるかに容易にします。もちろん、並行環境でガベージコレクションを実装すること自体が課題ですが、すべてのプログラムでなく一度だけそれを解決することで、誰もが恩恵を受けます。
最後に、並行性とは別に、ガベージコレクションはインターフェースを簡素化します。なぜなら、インターフェース間でメモリがどのように管理されるかを指定する必要がないからです。
これは、Rustのような言語におけるリソース管理の問題に新しいアイデアをもたらす最近の取り組みが誤っていると言っているわけではありません。私たちはこの取り組みを奨励し、それがどのように進化するかを楽しみにしています。しかし、Goはガベージコレクションを通じて、そしてガベージコレクションのみを通じてオブジェクトの寿命に対処するという、より伝統的なアプローチを採用しています。
現在の実装はマーク&スイープコレクタです。マシンがマルチプロセッサの場合、コレクタはメインプログラムと並行して別のCPUコアで実行されます。近年コレクタに対して行われた主要な作業により、巨大なヒープでも一時停止時間はしばしばミリ秒未満に短縮され、ネットワークサーバーにおけるガベージコレクションの主要な反対意見の1つをほぼ排除しました。アルゴリズムの改良、オーバーヘッドとレイテンシのさらなる削減、新しいアプローチの探索に関する作業は継続しています。GoチームのRick Hudsonによる2018年のISMM基調講演は、これまでの進捗状況と将来のアプローチについて説明しています。
パフォーマンスに関して言えば、Goはプログラマにメモリレイアウトと割り当てに関してかなりの制御を提供します。これはガベージコレクション言語では一般的ではありません。慎重なプログラマは、言語をうまく利用することでガベージコレクションのオーバーヘッドを劇的に削減できます。Goプログラムのプロファイリングに関する記事で、Goのプロファイリングツールのデモンストレーションを含む実際の例をご覧ください。