よくある質問 (FAQ)
起源
プロジェクトの目的は何ですか?
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で入手できます。
プロジェクトの歴史は何ですか?
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の成功は私たちの予想をはるかに超えています。
ゴーファーのマスコットの由来は何ですか?
マスコットとロゴは、Renée Frenchによってデザインされました。彼女はまた、Plan 9のウサギであるGlendaもデザインしました。ゴーファーに関するブログ記事では、数年前に彼女がWFMUのTシャツのデザインに使用したものがどのように派生したかについて説明しています。ロゴとマスコットは、Creative Commons Attribution 4.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での作業は高速であることを目的としています。1台のコンピューターで大きな実行可能ファイルを構築するのに数秒しかかからないはずです。これらの目標を達成するために、現在の言語からのプログラミングアプローチのいくつかを見直す必要がありました。これにより、階層型ではなく構成的な型システム、並行性とガベージコレクションのサポート、依存関係の厳格な仕様などが生まれました。これらはライブラリやツールではうまく処理できません。新しい言語が必要でした。
記事Go at Googleでは、Go言語の設計の背景と動機について説明するとともに、このFAQで提示された多くの回答についてより詳細に説明しています。
Goの先祖は何ですか?
Goは主にCファミリ(基本的な構文)にあり、Pascal/Modula/Oberonファミリ(宣言、パッケージ)からの重要な入力と、Tony HoareのCSPに触発された言語(NewsqueakやLimbo(並行性)など)からのいくつかのアイデアがあります。ただし、それは全面的に新しい言語です。あらゆる点で、この言語は、プログラマーが何をするか、そしてプログラミング、少なくとも私たちが行う種類のプログラミングをより効果的に、つまりより楽しくする方法を考えることによって設計されました。
設計における指針は何ですか?
Goが設計されたとき、JavaとC++は、少なくともGoogleでは、サーバーを記述するために最も一般的に使用されていた言語でした。私たちは、これらの言語にはあまりにも多くの簿記と繰り返しが必要だと感じました。一部のプログラマーは、効率と型の安全性を犠牲にして、Pythonのようなより動的で流動的な言語に移行することで対応しました。効率、安全性、および流動性を単一の言語で実現可能にする必要があると感じました。
Goは、言葉の2つの意味でタイピングの量を減らそうとしています。その設計全体を通して、私たちは混乱と複雑さを軽減しようとしてきました。フォワード宣言はなく、ヘッダーファイルもありません。すべてが1回だけ宣言されます。初期化は表現力豊かで自動的であり、使いやすいものです。構文はクリーンでキーワードが少ないです。反復(foo.Foo* myFoo = new(foo.Foo)
)は、:=
の宣言と初期化の構成を使用した簡単な型導出によって削減されます。そして、おそらく最も根本的に、型階層はありません。型はただあり、それらの関係を発表する必要はありません。これらの単純化により、Goは生産性を犠牲にすることなく、表現力豊かで理解しやすくなります。
もう1つの重要な原則は、概念を直交させることです。メソッドは任意の型に実装できます。構造体はデータを表し、インターフェースは抽象化を表します。など。直交性により、物事が組み合わされたときに何が起こるかを理解しやすくなります。
使用法
Googleは内部でGoを使用していますか?
はい。GoはGoogle内部の製品で広く使用されています。1つの例は、Chromeバイナリやapt-get
パッケージなどの他の大規模なインストール可能なファイルを配信するGoogleのダウンロードサーバーdl.google.com
です。
Goは、Googleで使用されている唯一の言語ではありませんが、サイト信頼性エンジニアリング(SRE)や大規模なデータ処理など、多くの分野で主要な言語です。また、Google Cloudを実行するソフトウェアの重要な部分でもあります。
他にGoを使用している企業はありますか?
Goの使用は世界中で拡大しており、特にクラウドコンピューティング分野で顕著ですが、それだけに限ったことではありません。Goで記述された主要なクラウドインフラストラクチャプロジェクトの例としては、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を設計する際、7ビットASCIIの範囲に限定されないように、過度にASCII中心にならないようにする必要がありました。これは、識別子の範囲を7ビットASCIIの範囲から拡張することを意味しました。Goのルール(識別子文字はUnicodeで定義された文字または数字でなければならない)は、理解と実装が簡単ですが、制限があります。例えば、結合文字は設計上除外されており、これによりデーヴァナーガリー語など一部の言語が除外されています。
このルールには、もう1つの残念な結果があります。エクスポートされた識別子は、大文字で始まる必要があるため、一部の言語の文字から作成された識別子は、定義上エクスポートできません。今のところ、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, and Recoverの記事を参照してください。また、Errors are valuesのブログ投稿では、エラーは単なる値であるため、エラー処理でGoの完全な機能を展開できることを示し、Goでエラーをクリーンに処理するための1つのアプローチについて説明しています。
なぜGoにはアサーションがないのですか?
Goにはアサーションはありません。それらが非常に便利であることは否定できませんが、私たちの経験では、プログラマーは適切なエラー処理とレポートについて考えることを避けるために、それらを頼りに使用しています。適切なエラー処理とは、サーバーが致命的でないエラーの後でクラッシュするのではなく、動作を継続することを意味します。適切なエラーレポートとは、エラーが直接的かつ要点を得ており、プログラマーが大きなクラッシュトレースを解釈する必要がないことを意味します。正確なエラーは、エラーを確認するプログラマーがコードに精通していない場合に特に重要です。
これが論争の的となっていることは理解しています。Go言語とライブラリには、現代的な慣習とは異なる点が多数あります。それは、異なるアプローチを試してみる価値があると感じるためです。
なぜCSPの概念に基づいて並行処理を構築したのですか?
並行処理とマルチスレッドプログラミングは、時間の経過とともに、難しいという評判を得てきました。これは、pthreadsのような複雑な設計と、ミューテックス、条件変数、メモリバリアなどの低レベルの詳細を重視しすぎていることが原因の一部であると考えています。高レベルのインターフェースを使用すると、内部でミューテックスなどがある場合でも、はるかに単純なコードが可能になります。
並行処理に対する高レベルの言語サポートを提供する最も成功したモデルの1つは、ホーアの通信順次プロセス(CSP)に由来します。OccamとErlangは、CSPに由来する2つのよく知られた言語です。Goの並行処理プリミティブは、ファーストクラスオブジェクトとしてのチャネルの強力な概念が主な貢献である、異なるファミリーツリーに由来します。以前のいくつかの言語での経験から、CSPモデルは手続き型言語フレームワークによく適合することが示されています。
なぜスレッドではなくgoroutineなのですか?
goroutineは、並行処理を使いやすくするための要素の1つです。アイデアは、しばらく前から存在していますが、独立して実行する関数(コルーチン)を、一連のスレッドに多重化することです。コルーチンが、ブロッキングシステムコールを呼び出すなどしてブロックすると、ランタイムは、同じオペレーティングシステムスレッド上の他のコルーチンを、ブロックされないように、別の実行可能なスレッドに自動的に移動します。プログラマーはこれらを認識しません。それがポイントです。goroutineと呼ぶ結果は非常に安価です。スタック用のメモリ(数キロバイト)以外にほとんどオーバーヘッドはありません。
スタックを小さくするために、Goのランタイムはサイズ変更可能な、上限付きのスタックを使用します。新しく作成されたゴルーチンには数キロバイトが与えられ、これはほとんどの場合十分です。十分でない場合、ランタイムはスタックを格納するためのメモリを自動的に拡張(および縮小)し、多数のゴルーチンが控えめな量のメモリで存在できるようにします。CPUのオーバーヘッドは、関数呼び出しあたり平均して約3つの安価な命令です。同じアドレス空間で数十万のゴルーチンを作成することは現実的です。もしゴルーチンが単なるスレッドであった場合、システムリソースははるかに少ない数で枯渇してしまうでしょう。
なぜマップ操作はアトミックに定義されていないのですか?
長い議論の末、マップの典型的な使用法は複数のゴルーチンからの安全なアクセスを必要とせず、必要な場合でもマップはおそらくすでに同期されているより大きなデータ構造または計算の一部であると判断されました。したがって、すべてのマップ操作にmutexを掴むことを要求すると、ほとんどのプログラムが遅くなり、少数のプログラムに安全性をもたらすだけでしょう。これは簡単な決定ではありませんでしたが、制御されていないマップアクセスがプログラムをクラッシュさせる可能性があることを意味します。
言語は、アトミックなマップ更新を妨げるものではありません。信頼できないプログラムをホストする場合など、必要な場合は、実装でマップアクセスをインターロックできます。
マップアクセスが安全でないのは、更新が発生している場合のみです。すべてのゴルーチンが読み取りのみ(マップ内の要素の検索、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よりも一般的です。プレーンな「非ボックス化」整数などの組み込み型を含む、あらゆる種類のデータに対して定義できます。struct(クラス)に限定されません。
また、型階層がないため、Goの「オブジェクト」はC++やJavaなどの言語よりもはるかに軽量に感じられます。
メソッドの動的ディスパッチを取得するにはどうすればよいですか?
動的にディスパッチされたメソッドを持つ唯一の方法は、インターフェースを介することです。structまたはその他の具体的な型のメソッドは常に静的に解決されます。
なぜ型継承がないのですか?
オブジェクト指向プログラミングは、少なくとも最もよく知られている言語では、型間の関係に関する議論が多すぎ、その関係は自動的に導き出すことができることがよくあります。Goは異なるアプローチを取ります。
Goでは、2つの型が関連していることを事前に宣言することをプログラマーに要求するのではなく、型は、そのメソッドのサブセットを指定するインターフェースを自動的に満たします。簿記を減らすことに加えて、このアプローチには実際的な利点があります。型は、従来の多重継承の複雑さなしに、一度に多くのインターフェースを満たすことができます。インターフェースは非常に軽量にすることができます。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()
}
型は、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
一部のポリモーフィック型システムでの類似の状況とは異なり、T
はEqualer
を実装しません。T.Equal
の引数の型は、文字通り必要な型Equaler
ではなく、T
です。
Goでは、型システムはEqual
の引数を昇格させません。T2
の型で示されているように、それはプログラマーの責任です。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と等しくないのはなぜですか?
内部的には、インターフェースは型T
と値V
という2つの要素として実装されます。V
は、int
、struct
、ポインターなどの具体的な値であり、インターフェース自体になることはなく、型T
を持ちます。たとえば、int
値の3をインターフェースに格納する場合、結果のインターフェース値は、概略的に(T=int
、V=3
)となります。値V
は、インターフェースの動的な値とも呼ばれます。これは、特定のインターフェース変数がプログラムの実行中に異なる値V
(および対応する型T
)を持つ可能性があるためです。
インターフェース値は、V
とT
の両方が設定されていない場合(T=nil
、V
は設定されていない場合)にのみnil
になります。特に、nil
インターフェースは常にnil
型を保持します。型*int
のnil
ポインターをインターフェース値の中に格納すると、ポインターの値に関係なく、内部型は*int
になります(T=*int
、V=nil
)。したがって、このようなインターフェース値は、内部のポインター値V
がnil
の場合でも、nil
にはなりません。
この状況は混乱を招く可能性があり、error
の戻り値など、nil
値がインターフェース値の中に格納された場合に発生します。
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つだけを取ることを指定する方法を提供します。システムプログラミングにおける一般的な例では、エラーがネットワークエラー、セキュリティエラー、またはアプリケーションエラーのいずれかであることを指定し、呼び出し元がエラーの型を調べることで問題の原因を識別できるようにします。別の例は、各ノードが異なる型(宣言、文、代入など)を持つことができる構文木です。
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桁の10進数で指定されており、その値を含む定数式は、float64
が保持できる範囲を超えて精度を維持します。定数または定数式が変数(プログラム内のメモリロケーション)に割り当てられた場合にのみ、通常の浮動小数点プロパティと精度を持つ「コンピューター」数値になります。
また、型付きの値ではなく単なる数値であるため、Goの定数は変数よりも自由に利用でき、厳密な変換規則に関連する不便さを軽減します。次のような式を記述できます。
sqrt2 := math.Sqrt(2)
理想的な数値2
は、math.Sqrt
の呼び出しのためにfloat64
に安全かつ正確に変換できるため、コンパイラーからの不満はありません。
Constantsというタイトルのブログ投稿で、このトピックについてさらに詳しく説明しています。
なぜマップは組み込みなのですか?
文字列と同じ理由です。それらは非常に強力で重要なデータ構造であるため、構文サポート付きの優れた実装を1つ提供することで、プログラミングがより快適になります。Goのマップの実装は、ほとんどの使用に対応できるほど強力であると考えています。特定のアプリケーションがカスタム実装の恩恵を受けることができる場合は、カスタム実装を作成できますが、構文的にそれほど便利ではありません。これは妥当なトレードオフであると思われます。
なぜマップはスライスをキーとして許可しないのですか?
マップのルックアップには等価演算子が必要ですが、スライスは実装していません。スライスが等価性を実装していないのは、等価性がそのような型では適切に定義されていないためです。浅い比較と深い比較、ポインターと値の比較、再帰的な型の処理方法など、複数の考慮事項があります。この問題を見直す可能性はありますが、スライスの等価性が何を意味するのかを明確に理解せずに、既存のプログラムを無効にすることなく、現時点ではそれを除外する方が簡単でした。
等価性は構造体と配列に対して定義されているため、マップキーとして使用できます。
なぜマップ、スライス、チャネルは参照であり、配列は値なのですか?
このトピックには多くの歴史があります。初期の頃は、マップとチャネルは構文的にはポインターであり、非ポインターインスタンスを宣言または使用することは不可能でした。また、配列がどのように機能するかについても苦労しました。最終的に、ポインターと値の厳密な分離は言語を使いにくくするという結論に至りました。これらの型を、関連する共有データ構造への参照として機能するように変更することで、これらの問題が解決しました。この変更により、言語にいくつかの残念な複雑さが追加されましたが、ユーザビリティに大きな影響を与えました。Goは、導入されたときにより生産的で快適な言語になりました。
コードの記述
ライブラリはどのように文書化されていますか?
コマンドラインからドキュメントにアクセスするには、goツールに、宣言、ファイル、パッケージなどのドキュメントへのテキストインターフェースを提供するdocサブコマンドがあります。
グローバルパッケージ検出ページpkg.go.dev/pkg/は、Web上のどこからでもGoソースコードからパッケージドキュメントを抽出し、宣言および関連要素へのリンク付きでHTMLとして提供するサーバーを実行します。これは、既存のGoライブラリについて学ぶ最も簡単な方法です。
プロジェクトの初期の頃には、ローカルマシン上のファイルのドキュメントを抽出するために実行できる同様のプログラムであるgodoc
がありました。pkg.go.dev/pkg/は本質的にその子孫です。別の後継は、godoc
のようにローカルで実行できるpkgsite
コマンドですが、まだgo
doc
によって表示される結果に統合されていません。
Goのプログラミングスタイルガイドはありますか?
明示的なスタイルガイドはありませんが、確かに認識できる「Goスタイル」はあります。
Goには、名前付け、レイアウト、ファイル編成に関する決定を導くための確立された規則があります。Effective Goというドキュメントには、これらのトピックに関するアドバイスがいくつか含まれています。より直接的には、プログラムgofmt
は、レイアウトルールを強制することを目的とするプリティプリンターです。これは、解釈を可能にする通常の「すべきこと」と「すべきでないこと」の要約を置き換えます。リポジトリ内のすべてのGoコードと、オープンソースの世界の大部分は、gofmt
を通して実行されています。
Go Code Review Commentsというタイトルのドキュメントは、プログラマーが見落としがちな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://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
開始方法の詳細については、チュートリアル:モジュールを作成するを参照してください。
モジュールを使用した依存関係の管理に関するガイドについては、モジュールの開発を参照してください。
モジュール内のパッケージは、進化するにつれて、インポート互換性ルールに従って、下位互換性を維持する必要があります。
古いパッケージと新しいパッケージが同じインポートパスを持っている場合、
新しいパッケージは、古いパッケージと下位互換性がある必要があります。
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.
1つの例外は、空のインターフェース型(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のことわざで要約されています。
メモリを共有して通信しないでください。代わりに、通信によってメモリを共有します。
この概念の詳細については、通信によるメモリの共有のコードウォークと、その関連する記事を参照してください。
大規模な同時実行プログラムは、これらのツールキットの両方から借用する可能性があります。
プログラムがより多くのCPUで高速に実行されないのはなぜですか?
プログラムの実行速度が、CPU の増加によって向上するかどうかは、解決しようとしている問題に依存します。Go 言語は、ゴルーチンやチャネルなどの並行処理プリミティブを提供しますが、並行処理が並列処理を可能にするのは、基盤となる問題が本質的に並列化可能な場合に限られます。本質的に逐次的な問題は、CPU を増やしても高速化できませんが、並列実行できる断片に分割できる問題は、高速化できる場合があり、時には劇的な高速化も可能です。
CPU を増やすことで、プログラムの実行速度が低下することもあります。実際的な観点からすると、有用な計算を行うよりも同期や通信に多くの時間を費やすプログラムは、複数の OS スレッドを使用するとパフォーマンスが低下する可能性があります。これは、スレッド間でデータをやり取りする際にコンテキストの切り替えが発生し、それには大きなコストがかかるためです。そのコストは、CPU が増えるにつれて増加する可能性があります。たとえば、Go の仕様書にある素数篩の例では、多くのゴルーチンが起動されるにもかかわらず、大きな並列性はありません。そのため、スレッド(CPU)の数を増やすと、高速化するよりも低速化する可能性が高くなります。
このトピックの詳細については、「並行処理は並列処理ではない」というタイトルの講演をご覧ください。
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 つの値を返す関数を記述できます。例を含む詳細な説明については、ブログ投稿「なぜジェネリクスなのか?」をご覧ください。
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 標準ライブラリは、インターフェース型を介してユーザーコードで表現できるため、定義していません。1 つの例外は、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つの選択肢があります。
-
リンク時に、可能なすべての動的インターフェースチェックのリストを作成し、それらを満たすが、コンパイルされたメソッドが欠落している型を探し、コンパイラーを再起動してそれらのメソッドを追加します。
これにより、リンク後に停止して一部のコンパイルを繰り返す必要が生じるため、ビルドが大幅に遅くなります。特に、インクリメンタルビルドが遅くなります。さらに悪いことに、新しくコンパイルされたメソッドコード自体に新しい動的インターフェースチェックが含まれている可能性があり、プロセスを繰り返す必要があります。プロセスが完了しない例を作成することもできます。
-
実行時に必要なメソッドコードをコンパイルする何らかの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
という単語が、メソッドの型引数の名前としてコンパイラーによって認識されるため、失敗します。コンパイラーのエラーメッセージは「s.f (文字列型の変数) に operator + が定義されていません
」のようになります。+
演算子は、事前に宣言された型 string
では正常に動作するため、これは混乱する可能性がありますが、宣言は、このメソッドに対して string
の定義を上書きしており、演算子は、その無関係なバージョンの string
では機能しません。このように事前に宣言された名前を上書きすることは有効ですが、それは奇妙なことであり、しばしば間違いです。
コンパイラーがプログラム内の型引数を推論できないのはなぜですか?
プログラマーがジェネリック型または関数の型引数が何であるかを簡単に理解できる多くのケースがありますが、言語はコンパイラーがそれを推論することを許可しません。型推論は、どの型が推論されるかについて混乱が生じないように意図的に制限されています。他の言語での経験は、予期しない型推論がプログラムの読み取りとデバッグ時にかなりの混乱を引き起こす可能性があることを示唆しています。呼び出しで使用する明示的な型引数を指定することは常に可能です。ルールがシンプルで明確なままであれば、将来、新しい形式の推論がサポートされる可能性があります。
パッケージとテスト
複数のファイルからなるパッケージを作成するにはどうすればよいですか?
パッケージのすべてのソースファイルを1つのディレクトリにまとめます。ソースファイルは、別のファイルのアイテムを自由に使用できます。前方宣言やヘッダーファイルは必要ありません。
パッケージは、複数のファイルに分割されている以外は、単一ファイルのパッケージと同じようにコンパイルおよびテストされます。
単体テストを作成するにはどうすればよいですか?
パッケージのソースと同じディレクトリに _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で記述されており、ELF/Mach-O/PEバイナリを生成するために、Goで記述されたカスタムローダー(ただし、Plan 9ローダーに基づいています)を使用しています。
Gccgo
コンパイラーは、標準のGCCバックエンドに結合された再帰下降パーサーを備えたC++で記述されたフロントエンドです。実験的な LLVMバックエンドは、同じフロントエンドを使用しています。
プロジェクトの開始時に、gc
にLLVMを使用することを検討しましたが、パフォーマンス目標を達成するには大きすぎて遅すぎると判断しました。後になってより重要なことは、LLVMから始めることで、Goが必要とするが標準のCセットアップの一部ではない、スタック管理など、ABIおよび関連する変更の一部を導入することが難しくなったということです。
Goは、Goコンパイラーを実装するのに適した言語であることが判明しましたが、それは当初の目標ではありませんでした。当初から自己ホスト型ではなかったため、Goの設計は、ネットワーク化されたサーバーという当初のユースケースに集中することができました。Goが初期の段階で自身をコンパイルする必要があることを決定した場合、コンパイラーの構築により適した言語になった可能性があります。これは価値のある目標ですが、最初に持っていた目標ではありませんでした。
gc
には独自の実装がありますが、ネイティブレクサーとパーサーは go/parser
パッケージで利用でき、ネイティブの型チェッカーもあります。gc
コンパイラーは、これらのライブラリのバリアントを使用します。
ランタイムサポートはどのように実装されていますか?
ここでもブートストラップの問題のため、ランタイムコードは元々ほとんどC(わずかなアセンブラー)で記述されていましたが、その後(一部のアセンブラービットを除いて)Goに変換されました。Gccgo
のランタイムサポートは glibc
を使用します。gccgo
コンパイラーは、ゴールドリンカーに対する最近の変更によってサポートされているセグメント化されたスタックと呼ばれる手法を使用してゴルーチンを実装します。同様に、Gollvm
は対応するLLVMインフラストラクチャ上に構築されています。
自明なプログラムのバイナリがこのように大きいのはなぜですか?
gc
ツールチェーンのリンカーは、デフォルトで静的にリンクされたバイナリを作成します。したがって、すべてのGoバイナリには、動的型チェック、リフレクション、さらにはパニック時のスタックトレースをサポートするために必要なランタイム型情報とともに、Goランタイムが含まれています。
Linux 上で gcc を使用して静的にコンパイルおよびリンクされた簡単な C の「hello, world」プログラムは、printf
の実装を含めて約 750 kB です。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 プログラマーは、goimports というツールを使用しています。このツールは、Go ソースファイルを自動的に書き換えて正しいインポートになるようにすることで、実際には未使用のインポートの問題を解消します。このプログラムは、Go ソースファイルが書き込まれるときに自動的に実行されるように、ほとんどのエディターおよび IDE に簡単に接続できます。この機能は、上記で説明したように、gopls
にも組み込まれています。
ウイルス対策ソフトウェアが Go ディストリビューションまたはコンパイルされたバイナリが感染していると判断するのはなぜですか?
これは、特に Windows マシンでよく発生する現象であり、ほぼ常に誤検出です。商用のウイルス対策プログラムは、Go バイナリの構造に混乱することがよくあります。これは、他の言語からコンパイルされたバイナリほど頻繁に見られないためです。
Go ディストリビューションをインストールしたばかりで、システムが感染していると報告している場合は、それは確かに間違いです。念のため、ダウンロードページのチェックサムと比較して、ダウンロードを検証できます。
いずれにせよ、レポートが誤りであると思われる場合は、ウイルススキャナーのサプライヤーにバグを報告してください。やがてウイルススキャナーが Go プログラムを理解できるようになるかもしれません。
パフォーマンス
Go がベンチマーク X でパフォーマンスが悪いのはなぜですか?
Go の設計目標の 1 つは、同等のプログラムで C のパフォーマンスに近づくことですが、一部のベンチマークでは、golang.org/x/exp/shootout にあるものを含め、非常にパフォーマンスが低い場合があります。最も遅いものは、同等のパフォーマンスのバージョンが Go で利用できないライブラリに依存しています。たとえば、pidigits.go は、多精度数学パッケージに依存しており、Go バージョンとは異なり、C バージョンは 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 からトリックを借用します。ステートメントを区切るセミコロンは正式な文法にありますが、先読みなしで、ステートメントの終わりになりうる行の終わりにレクサーによって自動的に挿入されます。これは実際には非常にうまく機能しますが、中括弧のスタイルを強制する効果があります。たとえば、関数の開き中括弧は、それ自体で1行に表示することはできません。
一部の人は、レクサーが次の行に中括弧を置くことを許可するために先読みを行うべきだと主張しています。同意できません。Go コードは gofmt
によって自動的にフォーマットされることを意図しているため、何らかのスタイルを選択する必要があります。そのスタイルは、C や Java で使用したものとは異なる場合がありますが、Go は異なる言語であり、gofmt
のスタイルは他のどのスタイルとも同様に優れています。さらに重要なことは、すべての Go プログラムに対して単一のプログラムで義務付けられた形式の利点が、特定のスタイルの認識された欠点を大幅に上回ることです。また、Go のスタイルは、Go のインタラクティブな実装が特別なルールなしに標準構文を一度に 1 行ずつ使用できることを意味することにも注意してください。
なぜガベージコレクションを行うのですか?コストがかかりすぎませんか?
システムプログラムにおける簿記の最大の要因の1つは、割り当てられたオブジェクトの有効期間を管理することです。手動で行われる C などの言語では、プログラマーの時間を大幅に消費する可能性があり、しばしば悪質なバグの原因となります。支援するメカニズムを提供する C++ や Rust などの言語であっても、それらのメカニズムはソフトウェアの設計に大きな影響を与える可能性があり、独自のプログラミングオーバーヘッドを追加することがよくあります。このようなプログラマーのオーバーヘッドを排除することが重要だと感じており、ここ数年間のガベージコレクションテクノロジーの進歩により、十分に安価に、十分な低遅延で実装できるため、ネットワーク化されたシステムで実行可能なアプローチになる可能性があると確信しています。
同時プログラミングの難しさの多くは、オブジェクトの有効期間の問題に根ざしています。オブジェクトがスレッド間で受け渡されるにつれて、それらが安全に解放されることを保証するのが面倒になります。自動ガベージコレクションにより、同時実行コードをはるかに簡単に記述できるようになります。もちろん、同時環境でガベージコレクションを実装すること自体が課題ですが、すべてのプログラムではなく一度だけそれを満たすことは、すべての人を助けます。
最後に、同時実行は別として、ガベージコレクションはインターフェースを単純化します。なぜなら、インターフェースはそれらを横断したメモリがどのように管理されるかを指定する必要がないためです。
これは、リソース管理の問題に新しいアイデアをもたらす Rust のような言語での最近の作業が誤ったものであると言うことではありません。この作業を奨励し、それがどのように進化するかを見るのを楽しみにしています。ただし、Go はガベージコレクション、そしてガベージコレクションのみを通じてオブジェクトの有効期間に対処するという、より伝統的なアプローチを採用しています。
現在の実装はマークアンドスイープコレクターです。マシンがマルチプロセッサーの場合、コレクターはメインプログラムと並行して別の CPU コアで実行されます。近年、コレクターに対する大規模な取り組みにより、一時停止時間が、大きなヒープの場合でも、多くの場合ミリ秒未満の範囲に短縮され、ネットワークサーバーにおけるガベージコレクションに対する主な異議の1つがほぼ解消されました。アルゴリズムの改良、オーバーヘッドと遅延のさらなる削減、および新しいアプローチの探索が続けられています。Go チームの Rick Hudson による 2018 年の ISMM 基調講演では、これまでの進捗状況と今後のアプローチについて説明しています。
パフォーマンスについて言えば、Go はガベージコレクション言語で一般的であるよりもはるかに、メモリレイアウトと割り当てを大幅に制御できることに注意してください。注意深いプログラマーは、言語を適切に使用することでガベージコレクションのオーバーヘッドを大幅に削減できます。Go のプロファイリングツールを示した例を含む、Go プログラムのプロファイリングに関する記事を参照してください。