Синтаксис объявления переменных в 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” от Дэвида Андерсона.