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)(a, b)を書くとintを返す関数を呼び出すため、fpは関数へのポインタです。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の構文

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

Cファミリー以外の言語は、通常、宣言に異なる型構文を使用します。別のポイントですが、名前が最初に来るのが一般的で、多くの場合コロンが続きます。したがって、上記の例は(架空の例ですが)次のようになります。

x int
p *int
a [3]int

これらの宣言は冗長ですが明確です。左から右に読むだけです。Goはここからヒントを得ますが、簡潔にするためにコロンを削除し、キーワードの一部を削除します

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

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

それでは、関数を考えてみましょう。Goの実際のmain関数は引数を取りませんが、Goで読み取られるようにmainの宣言を書き直してみましょう

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

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

func main(int, []string) int

パラメータ名を削除しても、同じように明確です。常に最初に来るので、混乱はありません。

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

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

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

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

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

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

型と式の構文の区別により、Goでクロージャを簡単に記述して呼び出すことができます

ポインタ

var a []int
x = a[1]

ポインタは規則を証明する例外です。たとえば、配列とスライスでは、Goの型構文は大括弧を型の左側に配置しますが、式の構文は大括弧を式の右側に配置します

var p *int
x = *p

わかりやすくするために、GoのポインタはCの*表記を使用していますが、ポインタ型についても同様の反転を行うことはできませんでした。したがって、ポインタは次のように機能します

var p *int
x = p*

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

var p ^int
x = p^

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

[]int("hi")

おそらくそうすべきでした(そしてxorには別の演算子を選択すべきでした)。なぜなら、型と式の両方に接頭辞アスタリスクを付けると、さまざまな方法で複雑になるからです。たとえば、次のように書くことができます

(*int)(nil)

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

ポインタ構文として*をあきらめていれば、これらの括弧は不要でした。

したがって、Goのポインタ構文は使い慣れたCの形式に関連付けられていますが、これらの関連付けは、文法で型と式を明確にするために括弧の使用を完全に止めることができないことを意味します。

全体として、Goの型構文は、特に複雑な場合、Cの構文よりも理解しやすいと考えています。

注記

Goの宣言は左から右に読みます。Cの宣言はらせん状に読むと指摘されています! David Andersonによる「時計回り/スパイラルルール」を参照してください。
次の記事:通信によるメモリ共有
前の記事:Google I/OのGoプログラミングセッションビデオ