Владислав Андреев - Блог

Синтаксис объявления переменных в Go

Это перевод статьи Go’s declaration syntax из официального блога The Go Blog.

Новички в Go интересуются, почему синтаксис объявления переменных и функций так отличается от традиционно принятого в С-подобных языках. В этом посте мы сравним оба подхода и объясним, почему объявления в Go выглядят так, как есть.

Синтаксис С

Сначала давайте обсудим синтаксис С. В С принят необычный и разумный подход к синтаксису деклараций. Вместо описания типов с помощью особых конструкций, пишут выражение, включающее объявляемый элемент и указывают, какой тип будет у выражения. Таким образом,

int x

объявляет x как int: выражение x будет иметь тип int.

В общем, чтобы понять, как записать тип новой переменной, напишите выражение, включающее эту переменную (которая будет иметь значение желаемого типа), а затем укажите тип слева от этой переменной.

Итак, следующие объявления

int *p;
int a[3];

говорят, что p это указатель на int, потому что у *p тип int. А a — это массив из int, потому что a[3] (игнорируя определённое значение индекса, которое прикреплено к размеру массива) имеет тип int.

Что насчёт функций? Изначально объявление типов аргументов функций С выносились за скобки, вот так:

int main(argc, argv)
  int argc;
  char *argv[];
{ /* ... тело функции ... */ }

И снова мы видим, что main это функция, потому что выражение main(argc, argv) возвращает значение типа int. В современном стиле мы бы написали:

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

но базовая структура одинакова.

Это неглупая идея для синтаксиса, хорошо работающая для простых типов, но может стать сбивающей с толку очень быстро. Рассмотрим известный пример объявления указателя на функцию. Следуйте правилам и у вас получится вот так:

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

Здесь fp - это указатель на функцию, потому что если вы напишите выражение (*fp)(a, b), то вызовите функцию, возвращающую выражение с типом int. А что, если один из аргументов fp - это функция?

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.

Можно представить больше примеров, но уже приведённые должны показать некоторые трудности, которые даёт синтаксис объявлений в языке С.

Есть ещё один момент, о котором надо сказать. Из-за того, что синтаксис объявлений одинаков, может быть тяжело распарсить выражения с типами в середине. Поэтому, например, тип, к которому приводят выражение, всегда берётся в скобки, как в этом примере:

(int)M_PI

Синтаксис Go

Языки, не относящиеся к С-подобным, обычно используют определённый синтаксис типов в объявлениях. Хотя это и отдельная тема, имя, обычно, идёт первым, часто сопровождаясь двоеточием. Таким образом, наши примеры ниже станут чем-то вроде (псевдокод, который доступно покажет пример):

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

Эти объявления понятны, ведь вы читаете их слева направо. Go взял кое-что себе отсюда, но в интересах краткости двоеточие удаляется, как и удаляются некоторые из ключевых слов.

x int
p *int
a [3]int

Нет прямой зависимости между тем, как выглядит [3]int и как его использовать в выражении (мы ещё дойдём до указателей в следующем разделе). Вы приобритаете ясность за счёт отдельной конструкции.

Теперь рассмотрим функции. Давайте перепишем объявление функции main, каким оно было бы в Go, хоть настоящая функция main в Go и не принимает аргументов:

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

Внешне это не сильно отличается от C, кроме замены char на массив строк, но он хорошо читается слева направо: функция main принимает int и slice из strings и возвращает int.

Опустите имена параметров и всё будет так же ясно - они всегда шли первыми и путаницы не будет.

func main(int, []string) int

Одно из достоинств этого стиля “слева-направо” - это насколько хорошо он работает по мере усложнения типов. Вот объявление переменной-функции (аналог указателя на функцию в 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^

и, возможно, нам следовало (и мы бы выбрали другой оператор для xor), потому что префикс * в типах и выражениях вместе всё усложняет. Скажем, хоть и можно написать вот так

[]int("hi")

для преобразования, нужно брать в скобки тип, если он начинается с *:

(*int)(nil)

Если бы мы были готовы отказаться от * как от части синтаксиса указателей, эти скобки были бы не нужны.

В общем, синтаксис Go для указателей связан со знакомой структурой в С, но эти связи означают, что мы не можем полностью отказаться от использования круглых скобок, чтобы различать типы и выражения в грамматике.

В целом, однако, мы считаем, что синтаксис типов в Go понять легче, чем в C, особенно, когда вещи становятся сложными.

Объявления в Go читаются слева направо. Выяснилось, что в C они читаются по спирали! Почитайте “Clockwise/Spiral Rule” от Дэвида Андерсона.