Go Wiki: GoForCPPProgrammers
Goは、C++のような汎用システム言語を目的としたシステムプログラミング言語です。これは、経験豊富なC++プログラマー向けのGoに関するメモです。このドキュメントでは、GoとC++の違いについて説明しており、類似点についてはほとんど言及していません。
念頭に置いておくべき重要な点は、それぞれの言語で熟練するために必要な思考プロセスに根本的な違いがあるということです。最も大きな違いは、C++のオブジェクトモデルがクラスとクラスの階層に基づいているのに対し、Goのオブジェクトモデルはインターフェースに基づいている(そして基本的にフラットである)ことです。その結果、C++のデザインパターンがGoにそのまま翻訳されることはほとんどありません。Goで効果的にプログラミングするには、問題を解決するためにC++で使用する可能性のあるメカニズムではなく、解決しようとしている問題を考慮する必要があります。
Goのより一般的な概要については、Goツアー、Goコードの書き方、効果的なGoを参照してください。
Go言語の詳細な説明については、Goの仕様を参照してください。
概念的な違い
- Goには、コンストラクターやデストラクターを持つクラスはありません。クラスメソッド、クラス継承階層、仮想関数の代わりに、Goはインターフェースを提供します。これについては、以下で詳しく説明します。インターフェースは、C++がテンプレートを使用する場合にも使用されます。
- Goは、割り当てられたメモリの自動ガベージコレクションを提供します。明示的にメモリを解放する必要はありません(また、解放することもできません)。ヒープ割り当てストレージとスタック割り当てストレージ、
new
vs.malloc
、delete
vs.delete[]
vs.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
キーワードを使用すると、フォールスルーさせることができます。これは隣接する case にも適用されます。
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 は enum をサポートしていません。代わりに、単一の 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
は構文エラーです。
おそらく驚くべきことに、new
は Go プログラムではあまり使用されません。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
から継承されました。型は複数のインターフェースを満たすことができます。
匿名フィールドを使用すると、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()
をオーバーロードするクラスです。たとえば、次のコードは、配列 (in
) の各要素に指定された単項演算子 (op
) を適用し、結果を別の配列 (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;
}
Goでの典型的なmy_transform
のバージョンは、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
}))
}
(ここでは、書き込み先のout
をmy_transform
に渡すのではなく、out
を返すようにしました。これは美的な判断によるもので、その点ではC++バージョンに似たコードにすることもできました。)
Goでは、関数は常に完全なクロージャであり、C++11の[&]
に相当します。重要な違いは、C++11では、クロージャがスコープが消滅した変数(上方funarg—ローカル変数を参照するラムダを返す関数—によって引き起こされる可能性がある)を参照することは無効であるということです。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の一部です。