はじめに

Goに新しく入った人は、宣言構文がCファミリーで確立された伝統と異なる理由を不思議に思います。この投稿では、2つのアプローチを比較し、Goの宣言がどのように見えるのかを説明します。

C構文

まず、C構文について話しましょう。Cは宣言構文に対して異常で巧妙なアプローチを取りました。特別な構文で型を記述するのではなく、宣言される項目を含む式を書き、その式がどの型になるかを述べます。したがって、

  1. int x;

declares x to be an int: the expression ‘x’ will have type int. 一般的に、新しい変数の型を書く方法を考えるには、その変数を含む式を書き、それが基本型に評価されるようにし、基本型を左に、式を右に置きます。

したがって、宣言は

  1. int *p;
  2. int a[3];

pがintへのポインタであることを示し、aがintの配列であることを示します。なぜなら、a[3](特定のインデックス値を無視すると、配列のサイズとして扱われる)にはint型があるからです。

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

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

再び、mainが関数であることがわかります。なぜなら、式main(argc, argv)がintを返すからです。現代の表記では、

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

と書きますが、基本的な構造は同じです。

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

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

ここで、fpは関数へのポインタです。なぜなら、式(*fp)(a, b)を書くと、intを返す関数を呼び出すからです。もしfpの引数の1つが関数自体であったらどうなりますか?

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

それは読みづらくなり始めます。

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

  1. int main(int, char *[])

argvは次のように宣言されます。

  1. char *argv[]

そのため、宣言の中間から名前を省略して型を構築します。ただし、char *[]の型を宣言するために名前を中間に置くことが明らかではありません。

そして、fpの宣言でパラメータの名前を付けないとどうなるか見てみましょう。

  1. int (*fp)(int (*)(int, int), int)

名前をどこに置くべきかが明らかではないだけでなく、これは関数ポインタの宣言であることも明確ではありません。戻り値の型が関数ポインタであったらどうなりますか?

  1. int (*(*fp)(int (*)(int, int), int))(int, int)

この宣言がfpに関するものであることを見つけるのは難しいです。

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

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

  1. (int)M_PI

Go構文

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

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

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

  1. x int
  2. p *int
  3. a [3]int

[3]intの見た目と、式でaを使用する方法との間に直接的な対応関係はありません。(ポインタについては次のセクションで戻ります。)明確さを得るために、別の構文のコストがかかります。

次に関数を考えてみましょう。mainの宣言をGoでどのように読むかを書き写してみましょう。ただし、Goの実際のmain関数は引数を取らないことに注意してください。

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

表面的には、Cとあまり変わりませんが、char配列から文字列への変更を除けば、左から右に読むのが良いです:

function main takes an int and a slice of strings and returns an int.

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

  1. func main(int, []string) int

この左から右のスタイルの利点の1つは、型がより複雑になるにつれてどれだけうまく機能するかです。ここに関数変数の宣言があります(Cの関数ポインタに類似):

  1. f func(func(int,int) int, int) int

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

  1. f func(func(int,int) int, int) func(int, int) int

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

型と式の構文の違いにより、Goでクロージャを簡単に書いて呼び出すことができます:

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

ポインタ

ポインタはルールを証明する例外です。配列やスライスでは、Goの型構文は型の左側にブラケットを置きますが、式構文は式の右側に置きます。

  1. var a []int
  2. x = a[1]

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

  1. var p *int
  2. x = *p

私たちは次のように言うことはできません。

  1. var p *int
  2. x = p*

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

  1. var p ^int
  2. x = p^

おそらくそうすべきでした(xorのために別の演算子を選んで)、型と式の両方にプレフィックスアスタリスクがあることは、いくつかの方法で物事を複雑にします。たとえば、

  1. []int("hi")

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

  1. (*int)(nil)

*をポインタ構文として放棄することをいとわなければ、これらの括弧は不要です。

したがって、Goのポインタ構文は親しみやすいC形式に結びついていますが、その結びつきは、文法において型と式を明確にするために括弧を使用することを完全に放棄することができないことを意味します。

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

ノート

Goの宣言は左から右に読みます。Cは螺旋状に読むと指摘されています! The “Clockwise/Spiral Rule” by David Anderson.