Go Wiki: GoForCPPProgrammers
Go は、C++ のように汎用的なシステム言語として意図されたシステムプログラミング言語です。これは、経験豊富な C++ プログラマー向け Go のメモです。このドキュメントでは、Go と C++ の違いについて説明し、類似点についてはほとんど言及しません。
重要な点は、両方の言語で習熟するために必要な思考プロセスにはいくつかの根本的な違いがあることです。最も手ごわいのは、C++ のオブジェクトモデルがクラスとクラス階層に基づいているのに対し、Go のオブジェクトモデルはインターフェースに基づいている(そして本質的にフラットである)ことです。その結果、C++ のデザインパターンは Go に文字通りに翻訳されることはめったにありません。Go で効果的にプログラミングするには、C++ で問題を解決するために使用するメカニズムではなく、解決する「問題」を考慮する必要があります。
Go のより一般的な紹介については、Go Tour、How to Write Go Code、および Effective Go を参照してください。
Go 言語の詳細については、Go spec を参照してください。
概念的な違い
- Go にはコンストラクタまたはデストラクタを持つクラスはありません。クラスメソッド、クラス継承階層、および仮想関数の代わりに、Go は「インターフェース」を提供します。これについては以下で詳しく説明します。インターフェースは C++ がテンプレートを使用する場所でも使用されます。
- Go は割り当てられたメモリの自動ガベージコレクションを提供します。メモリを明示的に解放する必要はありません(また、できません)。ヒープ割り当てストレージとスタック割り当てストレージ、
newとmalloc、またはdeleteとdelete[]とfreeについて心配する必要はありません。std::unique_ptr、std::shared_ptr、std::weak_ptr、std::auto_ptr、および通常の非スマートな「生」ポインタを個別に管理する必要はありません。Go のランタイムシステムは、プログラマに代わってこれらすべてのエラーを起こしやすいコードを処理します。 - Go にはポインタがありますが、ポインタ演算はありません。したがって、Go のポインタは C++ の参照に似ています。Go のポインタ変数を使用して文字列のバイトをウォークスルーすることはできません。スライス(以下で詳しく説明)は、ポインタ演算のほとんどの必要性を満たします。
- Go はデフォルトで「安全」です。ポインタは任意のメモリを指すことはできず、バッファオーバーランはクラッシュにつながり、セキュリティエクスプロイトにはなりません。
unsafeパッケージを使用すると、プログラマは明示的に要求された場合に Go の一部の保護メカニズムをバイパスできます。 - Go の配列はファーストクラスの値です。配列が関数パラメータとして使用される場合、関数は配列のコピーを受け取り、ポインタを受け取りません。ただし、実際には関数はパラメータにスライスを使用することがよくあります。スライスは基になる配列へのポインタを保持します。スライスについては以下で詳しく説明します。
- 文字列は言語によって提供されます。作成された後は変更できません。
- ハッシュテーブルは言語によって提供されます。これらはマップと呼ばれます。
- 個別の実行スレッドとそれらの間の通信チャネルは、言語によって提供されます。これについては以下で詳しく説明します。
- 特定の型(マップとチャネル、以下で詳しく説明)は、値ではなく参照で渡されます。つまり、マップを関数に渡してもマップはコピーされず、関数がマップを変更するとその変更は呼び出し元に表示されます。C++ の用語では、これらを参照型と考えることができます。
- Go はヘッダーファイルを使用しません。代わりに、各ソースファイルは定義された「パッケージ」の一部です。パッケージが大文字で始まる名前のオブジェクト(型、定数、変数、関数)を定義すると、そのオブジェクトは、そのパッケージをインポートする他のすべてのファイルから見えます。
- Go は暗黙の型変換をサポートしていません。異なる型を混合する操作にはキャスト(Go では変換と呼ばれます)が必要です。これは、同じ基になる型の異なるユーザー定義エイリアスにも当てはまります。
- Go は関数オーバーロードをサポートしておらず、ユーザー定義演算子もサポートしていません。
- Go は
constまたはvolatile修飾子をサポートしていません。 - Go は無効なポインタに
nilを使用します。C++ はNULLまたは単に0(または C++11 ではnullptr)を使用します。 - Go のイディオムでは、セントリー値(例:
-1)や構造化例外処理(C++ のtry…catchおよびthrow、または Go のpanic…recover)の代わりに、エラーを伝えるために複数の戻り値(1 つ以上のデータ結果とエラーコード)を使用します。
構文
宣言構文は C++ とは逆です。名前の後に型を記述します。C++ とは異なり、型の構文は変数が使用される方法と一致しません。型宣言は左から右に簡単に読み取ることができます。(var v1 int →「変数 v1 は int です。」)
//Go C++
var v1 int // int v1;
var v2 string // const std::string v2; (approximately)
var v3 [10]int // int v3[10];
var v4 []int // int* v4; (approximately)
var v5 struct { f int } // struct { int f; } v5;
var v6 *int // int* v6; (but no pointer arithmetic)
var v7 map[string]int // unordered_map<string, int>* v7; (approximately)
var v8 func(a int) int // int (*v8)(int a);
宣言は一般に、キーワードの後に宣言されるオブジェクトの名前が続く形式を取ります。キーワードは var、func、const、または type のいずれかです。メソッド宣言は、レシーバーが宣言されるオブジェクトの名前の前に表示されるという点でわずかな例外です。詳細については、「インターフェースの議論」を参照してください。
キーワードの後に括弧で囲まれた一連の宣言を続けることもできます。
var (
i int
m float64
)
関数を宣言する場合、各パラメータに名前を付けるか、どのパラメータにも名前を付けないかのいずれかを行う必要があります。(つまり、C++ は void f(int i, int); を許可しますが、Go は同等の func f(i int, int) を許可しません。)ただし、Go では便宜上、同じ型を持つ複数の名前をグループ化できます。
func f(i, j, k int, s, t string)
変数は宣言時に初期化できます。この場合、型の指定は許可されますが、必須ではありません。型が指定されていない場合、変数の型は初期化式の型になります。
var v = *p
定数については、以下の定数の議論も参照してください。変数が明示的に初期化されていない場合、型を指定する必要があります。その場合、型は暗黙的に型のゼロ値(0、nil など)に初期化されます。Go には初期化されていない変数はありません。
関数内では、:= を使用した短い宣言構文が使用できます。
v1 := v2 // C++11: auto v1 = v2;
これは次と同じです。
var v1 = v2 // C++11: auto v1 = v2;
Go は複数の代入を許可します。これらは並行して行われます。つまり、まず右側のすべての値が計算され、次にこれらの値が左側の変数に代入されます。
i, j = j, i // Swap i and j.
関数は、括弧で囲まれたリストで示される複数の戻り値を持つことができます。戻り値は、変数リストへの代入によって保存できます。
func f() (i int, j int) { ... }
v1, v2 = f()
複数の戻り値は、Go のエラー処理の主要なメカニズムです。
result, ok := g()
if !ok {
// Something bad happened.
return nil
}
// Continue as normal.
…
または、より簡潔に、
if result, ok := g(); !ok {
// Something bad happened.
return nil
}
// Continue as normal.
…
実際には、Go コードではセミコロンがほとんど使用されません。技術的には、すべての Go ステートメントはセミコロンで終了します。ただし、Go は、行が明らかに不完全でない限り(正確なルールは言語仕様にあります)、空白でない行の終わりをセミコロンとして扱います。この結果、場合によっては Go で改行を使用できないことがあります。たとえば、次のように記述することはできません。
func g()
{ // INVALID
}
g() の後にセミコロンが挿入され、関数定義ではなく関数宣言になります。同様に、次のように記述することはできません。
if x {
}
else { // INVALID
}
else の前の } の後にセミコロンが挿入され、構文エラーが発生します。
セミコロンはステートメントを終了するため、C++ と同じように引き続き使用できます。ただし、それは推奨されるスタイルではありません。Go のイディオムコードでは、不要なセミコロンを省略します。実際には、最初の for ループ句と、1 行に複数の短いステートメントが必要な場合を除いて、すべて省略されます。
このトピックについてですが、セミコロンとブレースの配置を心配するよりも、gofmt プログラムでコードをフォーマットすることをお勧めします。これにより、単一の標準 Go スタイルが生成され、フォーマットではなくコードに集中できるようになります。このスタイルは最初は奇妙に思えるかもしれませんが、他のどのスタイルと同じくらい優れており、慣れると快適になります。
構造体へのポインタを使用する場合、-> の代わりに . を使用します。したがって、構文的には、構造体と構造体へのポインタは同じ方法で使用されます。
type myStruct struct{ i int }
var v9 myStruct // v9 has structure type
var p9 *myStruct // p9 is a pointer to a structure
f(v9.i, p9.i)
Go は、if ステートメントの条件、for ステートメントの式、または switch ステートメントの値の周りに括弧を必要としません。一方、if または for ステートメントの本体の周りには波括弧が必要です。
if a < b { f() } // Valid
if (a < b) { f() } // Valid (condition is a parenthesized expression)
if (a < b) f() // INVALID
for i = 0; i < 10; i++ {} // Valid
for (i = 0; i < 10; i++) {} // INVALID
Go には while ステートメントも do/while ステートメントもありません。for ステートメントは単一の条件で使用でき、これは while ステートメントと同等になります。条件を完全に省略すると無限ループになります。
Go は break と continue にラベルを指定することを許可します。ラベルは for、switch、または select ステートメントを参照する必要があります。
switch ステートメントでは、case ラベルはフォールスルーしません。fallthrough キーワードを使用してフォールスルーさせることができます。これは隣接するケースにも適用されます。
switch i {
case 0: // empty case body
case 1:
f() // f is not called when i == 0!
}
しかし、1 つの case に複数の値を持たせることもできます。
switch i {
case 0, 1:
f() // f is called if i == 0 || i == 1.
}
case の値は定数である必要も、整数である必要もありません。文字列やポインタなど、等値比較演算子をサポートする任意の型を使用でき、switch 値が省略された場合はデフォルトで true になります。
switch {
case i < 0:
f1()
case i == 0:
f2()
case i > 0:
f3()
}
defer ステートメントは、defer ステートメントを含む関数が戻った後に、関数を呼び出すために使用できます。defer は C++ のデストラクタの代わりになることが多いですが、特定のクラスやオブジェクトではなく、呼び出し元のコードに関連付けられています。
fd := open("filename")
defer close(fd) // fd will be closed when this function returns.
演算子
++ および -- 演算子は、式ではなく、ステートメントでのみ使用できます。c = *p++ とは記述できません。*p++ は (*p)++ として解析されます。
演算子の優先順位が異なります。例として、4 & 3 << 1 は Go では 0 に、C++ では 4 に評価されます。
Go operator precedence:
1. * / % << >> & &^
2. + - | ^
3. == != < <= > >=
4. &&
5. ||
C++ operator precedence (only relevant operators):
1. * / %
2. + -
3. << >>
4. < <= > >=
5. == !=
6. &
7. ^
8. |
9. &&
10. ||
定数
Go では定数は「型なし」である場合があります。これは、宣言で型が指定されておらず、初期化式が型なしの定数のみを使用している場合、const 宣言で名前が付けられた定数にも適用されます。型なしの定数から派生した値は、型付きの値が必要なコンテキストで使用されるときに型付きになります。これにより、一般的な暗黙的な型変換を必要とせずに、定数を比較的自由に利用できます。
var a uint
f(a + 1) // untyped numeric constant "1" becomes typed as uint
言語は、型なしの数値定数または定数式のサイズに制限を課しません。制限は、型が必要な場所で定数が使用される場合にのみ適用されます。
const huge = 1 << 100
f(huge >> 98)
Go は列挙型をサポートしていません。代わりに、単一の const 宣言で特殊な名前 iota を使用して、一連の増加する値を取得できます。const の初期化式が省略されている場合、前の式が再利用されます。
const (
red = iota // red == 0
blue // blue == 1
green // green == 2
)
型
C++ と Go は類似していますが、同一ではない組み込み型を提供します。さまざまな幅の符号付きおよび符号なし整数、32 ビットおよび 64 ビット浮動小数点数(実数および複素数)、struct、ポインタなどです。Go では、uint8、int64、および同様の名前の整数型は言語の一部であり、実装依存のサイズを持つ整数(例:long long)の上に構築されていません。Go はさらに、ネイティブの string、map、および chan(チャネル)型、そしてファーストクラスの配列とスライス(以下で説明)を提供します。文字列は ASCII ではなく Unicode でエンコードされます。
Go は C++ よりもはるかに強い型付けをされています。特に、Go には暗黙的な型変換はなく、明示的な型変換のみです。これにより、追加の安全性とある種のエラーからの解放が提供されますが、いくつかの追加の型付けのコストがかかります。Go には union 型もありません。これは型システムを破壊する可能性があるためです。ただし、Go の interface{}(以下を参照)は、型安全な代替手段を提供します。
C++ と Go は両方とも型エイリアス(C++ では typedef、Go では type)をサポートしています。ただし、C++ とは異なり、Go はこれらを異なる型として扱います。したがって、C++ では次のものが有効です。
// C++
typedef double position;
typedef double velocity;
position pos = 218.0;
velocity vel = -9.8;
pos += vel;
しかし、Go では明示的な型変換がなければ同等のものは無効です。
type position float64
type velocity float64
var pos position = 218.0
var vel velocity = -9.8
pos += vel // INVALID: mismatched types position and velocity
// pos += position(vel) // Valid
エイリアス化されていない型でも同じです。int と uint は、一方をもう一方に明示的に変換しない限り、式で結合できません。
Go は C++ とは異なり、ポインタを整数にキャストしたり、整数からキャストしたりすることを許可しません。ただし、Go の unsafe パッケージを使用すると、必要に応じて(たとえば、低レベルのシステムコードで使用する場合など)この安全メカニズムを明示的にバイパスできます。
スライス
スライスは概念的には、配列へのポインタ、長さ、容量の3つのフィールドを持つ構造体です。スライスは、基になる配列の要素にアクセスするために [] 演算子をサポートします。組み込みの len 関数はスライスの長さを返します。組み込みの cap 関数は容量を返します。
配列、または別のスライスが与えられた場合、a[i:j] を介して新しいスライスが作成されます。これにより、a を参照し、インデックス i から始まり、インデックス j の前で終わる新しいスライスが作成されます。長さは j-i です。i が省略された場合、スライスは 0 から始まります。j が省略された場合、スライスは len(a) で終わります。新しいスライスは a が参照するのと同じ配列を参照します。このステートメントの 2 つの意味は、①新しいスライスを使用して行われた変更は a を使用して確認できること、および ②スライスの作成は(安価であるように意図されている)安価であることです。基になる配列のコピーを作成する必要はありません。新しいスライスの容量は、単に a の容量から i を引いたものです。配列の容量は配列の長さです。
これは、Go が C++ がポインタを使用するような一部のケースでスライスを使用することを意味します。型 [100]byte (100 バイトの配列、おそらくバッファ) の値を作成し、それをコピーせずに関数に渡したい場合は、関数パラメータを型 []byte として宣言し、配列のスライスを渡す必要があります (a[:] は配列全体を渡します)。C++ とは異なり、バッファの長さを渡す必要はありません。len を介して効率的にアクセスできます。
スライス構文は文字列でも使用できます。これは新しい文字列を返し、その値は元の文字列の部分文字列です。文字列は不変であるため、文字列スライスはスライスの内容に新しいストレージを割り当てることなく実装できます。
値の作成
Go には組み込み関数 new があり、型を受け取り、ヒープにスペースを割り当てます。割り当てられたスペースは、その型に対してゼロ初期化されます。たとえば、new(int) はヒープに新しい int を割り当て、値 0 で初期化し、そのアドレス(型 *int)を返します。C++ とは異なり、new は関数であり、演算子ではありません。new int は構文エラーです。
驚くべきことに、Go プログラムでは new はあまり一般的に使用されません。Go では、変数のアドレスを取ることは常に安全であり、ぶら下がりポインタを生成することはありません。プログラムが変数のアドレスを取る場合、必要に応じてヒープに割り当てられます。したがって、これらの関数は同等です。
type S struct { I int }
func f1() *S {
return new(S)
}
func f2() *S {
var s S
return &s
}
func f3() *S {
// More idiomatic: use composite literal syntax.
return &S{}
}
対照的に、C++ ではローカル変数へのポインタを返すのは安全ではありません。
// C++
S* f2() {
S s;
return &s; // INVALID -- contents can be overwritten at any time
}
マップおよびチャネルの値は、組み込み関数 make を使用して割り当てる必要があります。初期化子なしでマップまたはチャネル型で宣言された変数は、自動的に nil に初期化されます。make(map[int]int) を呼び出すと、型 map[int]int の新しく割り当てられた値が返されます。make はポインタではなく値を返すことに注意してください。これは、マップとチャネルの値が参照によって渡されるという事実と一致しています。マップ型で make を呼び出すと、マップの期待される容量であるオプションの引数が取られます。チャネル型で make を呼び出すと、チャネルのバッファリング容量を設定するオプションの引数が取られます。デフォルトは 0(バッファなし)です。
make 関数はスライスを割り当てるためにも使用できます。この場合、基になる配列のメモリを割り当て、それを参照するスライスを返します。必須の引数は 1 つで、これはスライスの要素数です。2 番目のオプションの引数は、スライスの容量です。たとえば、make([]int, 10, 20)。これは new([20]int)[0:10] と同じです。Go はガベージコレクションを使用するため、返されたスライスへの参照がなくなると、新しく割り当てられた配列はしばらくして破棄されます。
インターフェース
C++ がクラス、サブクラス、およびテンプレートを提供するのに対し、Go はインターフェースを提供します。Go のインターフェースは C++ の純粋仮想クラスに似ています。データメンバーがなく、すべてのメソッドが純粋仮想であるクラスです。ただし、Go では、インターフェースで指定されたメソッドを提供する任意の型を、インターフェースの実装として扱うことができます。明示的な継承の宣言は必要ありません。インターフェースの実装はインターフェース自体とは完全に分離されています。
メソッドは、通常の関数定義のように見えますが、「レシーバー」を持っています。レシーバーは、C++ のクラスメソッドの this ポインタに似ています。
type myType struct{ i int }
func (p *myType) Get() int { return p.i }
これは、myType に関連付けられたメソッド Get を宣言します。レシーバーは関数の本体で p と名付けられています。
メソッドは名前付き型で定義されます。値を異なる型に変換すると、新しい値には古い型のメソッドではなく、新しい型のメソッドが適用されます。
組み込み型から派生した新しい名前付き型を宣言することで、組み込み型にメソッドを定義できます。新しい型は組み込み型とは異なります。
type myInteger int
func (p myInteger) Get() int { return int(p) } // Conversion required.
func f(i int) {}
var v myInteger
// f(v) is invalid.
// f(int(v)) is valid; int(v) has no defined methods.
このインターフェースが与えられた場合、
type myInterface interface {
Get() int
Set(i int)
}
次を追加することで、myType をインターフェースに適合させることができます。
func (p *myType) Set(i int) { p.i = i }
これで、myInterface をパラメータとして取る関数は、型 *myType の変数を受け入れるようになります。
func GetAndSet(x myInterface) {}
func f1() {
var p myType
GetAndSet(&p)
}
言い換えれば、myInterface を C++ の純粋抽象基底クラスと見なした場合、*myType の Set と Get を定義することで、*myType は自動的に myInterface から継承されました。1 つの型は複数のインターフェースを満たすことができます。
匿名フィールドは、C++ の子クラスに非常によく似たものを実装するために使用できます。
type myChildType struct {
myType
j int
}
func (p *myChildType) Get() int { p.j++; return p.myType.Get() }
これは、myChildType を myType の子として効果的に実装します。
func f2() {
var p myChildType
GetAndSet(&p)
}
Set メソッドは myType から実質的に継承されます。匿名フィールドに関連付けられたメソッドは、それを囲む型のメソッドに昇格されるためです。この場合、myChildType は型 myType の匿名フィールドを持つため、myType のメソッドも myChildType のメソッドになります。この例では、Get メソッドはオーバーライドされ、Set メソッドは継承されました。
これは C++ の子クラスと厳密には同じではありません。匿名フィールドのメソッドが呼び出された場合、そのレシーバーは周囲の構造体ではなくフィールドです。言い換えれば、匿名フィールドのメソッドは仮想関数ではありません。仮想関数と同等のものが必要な場合は、インターフェースを使用してください。
インターフェース型を持つ変数は、型アサーションと呼ばれる特別な構文を使用して、異なるインターフェース型に変換できます。これは、C++ の dynamic_cast と同様に、実行時に動的に実装されます。dynamic_cast とは異なり、2 つのインターフェース間に宣言された関係は必要ありません。
type myPrintInterface interface {
Print()
}
func f3(x myInterface) {
x.(myPrintInterface).Print() // type assertion to myPrintInterface
}
myPrintInterface への変換は完全に動的です。x の動的型が Print メソッドを定義している限り、機能します。
この変換は動的なため、C++ のテンプレートに似た汎用プログラミングを実装するために使用できます。これは、最小限のインターフェースの値を操作することによって行われます。
type Any interface{}
コンテナは Any を使用して記述できますが、呼び出し元は、含まれる型の値を取得するために型アサーションを使用してアンボックスする必要があります。型付けは静的ではなく動的であるため、C++ テンプレートが関連する操作をインライン化する方法と同等のものはありません。操作は実行時に完全に型チェックされますが、すべての操作には関数呼び出しが伴います。
type Iterator interface {
Get() Any
Set(v Any)
Increment()
Equal(arg Iterator) bool
}
Equal には Iterator 型の引数があることに注意してください。これは C++ テンプレートのように動作しません。FAQ を参照してください。
関数クロージャ
C++11 以前の C++ バージョンでは、隠された状態を持つ関数を作成する最も一般的な方法は、「ファンクタ」を使用することでした。これは、operator() をオーバーロードしてインスタンスを関数のように見せるクラスです。たとえば、次のコードは、指定された単項演算子(op)を配列(in)の各要素に適用し、結果を別の配列(out)に格納する my_transform 関数(STL の std::transform の簡略版)を定義します。プレフィックス合計(つまり、{x[0]、x[0]+x[1]、x[0]+x[1]+x[2]、...})を実装するために、コードは実行中の合計(total)を追跡するファンクタ(MyFunctor)を作成し、このファンクタのインスタンスを my_transform に渡します。
// C++
#include <iostream>
#include <cstddef>
template <class UnaryOperator>
void my_transform (size_t n_elts, int* in, int* out, UnaryOperator op)
{
size_t i;
for (i = 0; i < n_elts; i++)
out[i] = op(in[i]);
}
class MyFunctor {
public:
int total;
int operator()(int v) {
total += v;
return total;
}
MyFunctor() : total(0) {}
};
int main (void)
{
int data[7] = {8, 6, 7, 5, 3, 0, 9};
int result[7];
MyFunctor accumulate;
my_transform(7, data, result, accumulate);
std::cout << "Result is [ ";
for (size_t i = 0; i < 7; i++)
std::cout << result[i] << ' ';
std::cout << "]\n";
return 0;
}
C++11 では、変数に格納したり関数に渡したりできる匿名(「ラムダ」)関数が追加されました。これらはオプションでクロージャとして機能し、親スコープから状態を参照できます。この機能は my_transform を大幅に簡素化します。
// C++11
#include <iostream>
#include <cstddef>
#include <functional>
void my_transform (size_t n_elts, int* in, int* out, std::function<int(int)> op)
{
size_t i;
for (i = 0; i < n_elts; i++)
out[i] = op(in[i]);
}
int main (void)
{
int data[7] = {8, 6, 7, 5, 3, 0, 9};
int result[7];
int total = 0;
my_transform(7, data, result, [&total] (int v) {
total += v;
return total;
});
std::cout << "Result is [ ";
for (size_t i = 0; i < 7; i++)
std::cout << result[i] << ' ';
std::cout << "]\n";
return 0;
}
my_transform の典型的な Go バージョンは、C++11 バージョンとよく似ています。
package main
import "fmt"
func my_transform(in []int, xform func(int) int) (out []int) {
out = make([]int, len(in))
for idx, val := range in {
out[idx] = xform(val)
}
return
}
func main() {
data := []int{8, 6, 7, 5, 3, 0, 9}
total := 0
fmt.Printf("Result is %v\n", my_transform(data, func(v int) int {
total += v
return total
}))
}
(注: my_transform から out を返すことを選択しました。これは、書き込み用の out を渡すのではなく、美的な判断でした。この点では C++ バージョンにより近いコードも書くことができました。)
Go では、関数は常に完全なクロージャであり、C++11 の [&] に相当します。重要な違いは、C++11 では、スコープがなくなった変数(上位のファンアグ、つまりローカル変数を参照するラムダを返す関数によって引き起こされる可能性があります)を参照するクロージャは無効であることです。Go では、これは完全に有効です。
並行処理
C++11 の std::thread と同様に、Go は共有アドレス空間で並行して実行される新しい実行スレッドを開始することを許可します。これらは「ゴルーチン」と呼ばれ、go ステートメントを使用して生成されます。典型的な std::thread の実装は重いオペレーティングシステムのスレッドを起動しますが、ゴルーチンは複数のオペレーティングシステムのスレッド間で多重化される軽量のユーザーレベルスレッドとして実装されます。したがって、ゴルーチンは(安価であるように意図されており)プログラム全体で自由に利用できます。
func server(i int) {
for {
fmt.Print(i)
time.Sleep(10 * time.Second)
}
}
go server(1)
go server(2)
(server 関数内の for 文は、C++ の while (true) ループと同じであることに注意してください。)
関数リテラル(Go がクロージャとして実装)は、go ステートメントで役立ちます。
var g int
go func(i int) {
s := 0
for j := 0; j < i; j++ {
s += j
}
g = s
}(1000) // Passes argument 1000 to the function literal.
C++11 と同様に、しかしそれ以前の C++ バージョンとは異なり、Go は同期されていないメモリへのアクセスに対するメモリモデルを定義しています。Go は sync パッケージに std::mutex の類似物を提供しますが、これは Go プログラムでスレッド間通信と同期を実装する通常の手段ではありません。代わりに、Go スレッドは通常、メッセージパッシングによって通信します。これはロックやバリアとは根本的に異なるアプローチです。この件に関する Go のモットーは次のとおりです。
メモリを共有して通信するのではなく、通信によってメモリを共有する。
つまり、「チャネル」はゴルーチン間の通信に使用されます。任意の型(他のチャネルを含む!)の値をチャネルで送信できます。チャネルはバッファなしまたはバッファ付き(チャネル構築時に指定されたバッファ長を使用)にできます。
チャネルはファーストクラスの値です。他の値と同様に、変数に格納したり、関数に渡したり、関数から受け取ったりできます。(関数に渡される場合、チャネルは参照渡しされます。)チャネルは型も持ちます。chan int は chan string とは異なります。
Go プログラムで非常に広く使用されているため、チャネルは(効率的で安価であるように意図されています)。チャネルで値を送信するには、バイナリ演算子として <- を使用します。チャネルで値を受信するには、単項演算子として <- を使用します。チャネルは複数の送信者と複数の受信者間で共有でき、送信された各値が最大で 1 つの受信者によって受信されることを保証します。
単一の値へのアクセスを制御するためにマネージャー関数を使用する例を次に示します。
type Cmd struct {
Get bool
Val int
}
func Manager(ch chan Cmd) {
val := 0
for {
c := <-ch
if c.Get {
c.Val = val
ch <- c
} else {
val = c.Val
}
}
}
この例では、同じチャネルが入力と出力に使用されています。これは、複数のゴルーチンが同時にマネージャーと通信している場合には正しくありません。マネージャーからの応答を待っているゴルーチンが、代わりに別のゴルーチンからの要求を受信する可能性があります。解決策は、チャネルを渡すことです。
type Cmd2 struct {
Get bool
Val int
Ch chan<- int
}
func Manager2(ch <-chan Cmd2) {
val := 0
for {
c := <-ch
if c.Get {
c.Ch <- val
} else {
val = c.Val
}
}
}
Manager2 を使用するには、それにチャネルを与えます。
func getFromManagedChannel(ch chan<- Cmd2) int {
myCh := make(chan int)
c := Cmd2{true, 0, myCh} // Composite literal syntax.
ch <- c
return <-myCh
}
func main() {
ch := make(chan Cmd2)
go Manager2(ch)
// ... some code ...
currentValue := getFromManagedChannel(ch)
// ... some more code...
}
このコンテンツはGo Wikiの一部です。