The Go Blog

Go の宣言構文

ロブ・パイク
2010年7月7日

はじめに

Go の新規ユーザーは、宣言構文が C ファミリーで確立された伝統と異なる理由を疑問に思います。この投稿では、2 つのアプローチを比較し、Go の宣言が現在のようになった理由を説明します。

C 構文

まず、C 構文について説明します。C は、宣言構文に対して珍しい、巧妙なアプローチを採用しました。特殊な構文で型を記述するのではなく、宣言される項目を含む式を記述し、その式がどのような型を持つかを指定します。したがって、

int x;

は x を int として宣言します。式「x」は int 型になります。一般的に、新しい変数の型を記述する方法を理解するには、その変数を含む式を記述して基本型に評価し、次に基本型を左に、式を右に置きます。

したがって、宣言は次のとおりです。

int *p;
int a[3];

は、' *p ' が int 型であるため p が int へのポインタであり、a[3] (特定のインデックス値を無視して配列のサイズと見なされる) が int 型であるため a が int の配列であることを示しています。

関数についてはどうでしょうか? 元々、C の関数宣言は、次のように引数の型を括弧の外に記述していました。

int main(argc, argv)
    int argc;
    char *argv[];
{ /* ... */ }

ここでも、式 main(argc, argv) が int を返すため、main が関数であることがわかります。現代の表記法では、次のように記述します。

int main(int argc, char *argv[]) { /* ... */ }

しかし、基本的な構造は同じです。

これは、単純な型にはうまく機能する巧妙な構文のアイデアですが、すぐに混乱を招く可能性があります。有名な例は、関数ポインタの宣言です。ルールに従うと、次のようになります。

int (*fp)(int a, int b);

ここで、fp は関数へのポインタです。なぜなら、式 (*fp)(a, b) を記述すると、int を返す関数が呼び出されるからです。fp の引数の 1 つがそれ自体が関数である場合はどうでしょうか。

int (*fp)(int (*ff)(int x, int y), int b)

これは読みにくくなり始めています。

もちろん、関数を宣言するときにパラメータ名を省略できます。したがって、main は次のように宣言できます。

int main(int, char *[])

argv は次のように宣言されることを思い出してください。

char *argv[]

したがって、宣言の途中から名前を削除して型を構築します。ただし、名前を真ん中に置くことで char *[] 型の何かを宣言することは明らかではありません。

パラメータに名前を付けない場合の fp の宣言を見てください。

int (*fp)(int (*)(int, int), int)

どこに名前を置くべきか明らかではないだけでなく、

int (*)(int, int)

それがそもそも関数ポインタの宣言であることは正確には明らかではありません。そして、戻り値の型が関数ポインタである場合はどうでしょうか。

int (*(*fp)(int (*)(int, int), int))(int, int)

この宣言が fp に関するものであることさえわかりにくいです。

さらに複雑な例を構築することもできますが、これらは C の宣言構文がもたらす可能性のあるいくつかの困難を示すはずです。

ただし、もう 1 つ述べるべき点があります。型と宣言構文が同じであるため、途中に型がある式を解析することが難しい場合があります。これが、例えば C のキャストが常に型を括弧で囲む理由です。

(int)M_PI

Go 構文

C ファミリー以外の言語は、通常、宣言で異なる型構文を使用します。別の点ですが、名前が最初に来ることが多く、その後にコロンが続くことがよくあります。したがって、上記の例は (架空ではありますが、説明的な言語で) 次のようになります。

x: int
p: pointer to int
a: array[3] of int

これらの宣言は、冗長ですが明確です。左から右に読み進めるだけです。Go はここからヒントを得ていますが、簡潔さのためにコロンを省略し、いくつかのキーワードを削除しています。

x int
p *int
a [3]int

[3]int の外観と、式で a を使用する方法との間に直接的な対応はありません。(ポインタについては次のセクションで戻ります。) 別個の構文を犠牲にして明確さを得ることができます。

次に、関数を検討します。Go の実際の main 関数は引数をとらないものの、Go で読み取れる main の宣言を転記してみましょう。

func main(argc int, argv []string) int

表面上は、`char` 配列から文字列への変更以外は C と大差ありませんが、左から右へ読むとよくわかります。

関数 main は int と文字列のスライスを取り、int を返します。

パラメータ名を省略しても同じくらい明確です。常に最初に来るので混乱はありません。

func main(int, []string) int

この左から右へのスタイルの利点の 1 つは、型がより複雑になったときにいかにうまく機能するかです。これは関数変数の宣言です (C の関数ポインタに類似しています)。

f func(func(int,int) int, int) int

または、f が関数を返す場合

f func(func(int,int) int, int) func(int, int) int

依然として左から右へと明確に読み取ることができ、どの名前が宣言されているかは常に明らかです。名前が最初に来るからです。

型と式の構文の区別により、Go でクロージャを記述して呼び出すのが容易になります。

sum := func(a, b int) int { return a+b } (3, 4)

ポインタ

ポインタは、規則の例外です。たとえば、配列やスライスでは、Go の型構文は型にブラケットを左に置きますが、式構文は式にブラケットを右に置くことに注意してください。

var a []int
x = a[1]

馴染みやすいように、Go のポインタは C の * 表記を使用しますが、ポインタ型に対して同様の反転を行うことはできませんでした。したがって、ポインタは次のように機能します。

var p *int
x = *p

我々は言うことはできませんでした。

var p *int
x = p*

なぜなら、その後置 * は乗算と混同されるからです。たとえば、Pascal の ^ を使用することもできました。

var p ^int
x = p^

そしておそらくそうすべきでした (そして排他的論理和の別の演算子を選択すべきでした)。なぜなら、型と式の両方に接頭辞アスタリスクを使用すると、いくつかの点で複雑になるからです。たとえば、次のように記述できます。

[]int("hi")

変換として、* で始まる型は括弧で囲む必要があります。

(*int)(nil)

* をポインタ構文として諦める気があれば、それらの括弧は不要だったでしょう。

したがって、Go のポインタ構文は馴染みのある C の形式と結びついていますが、その結びつきは、文法で型と式を曖昧さなく区別するために括弧を使用することから完全に離れることができないことを意味します。

全体として、特に複雑になった場合でも、Go の型構文は C のそれよりも理解しやすいと信じています。

Go の宣言は左から右に読み進めます。C の宣言は螺旋状に読まれることが指摘されています!David Anderson の「Clockwise/Spiral Rule」を参照してください。

次の記事:通信によってメモリを共有する
前の記事:Google I/O からの Go プログラミングセッションビデオ
ブログインデックス