Синтаксис объявления переменных в Go
Это перевод статьи Go’s declaration syntax из официального блога The Go Blog.
Новички в Go интересуются, почему синтаксис объявления переменных и функций так отличается от традиционно принятого в С-подобных языках. В этом посте мы сравним оба подхода и объясним, почему объявления в Go выглядят так, как есть.
Синтаксис С
Сначала давайте обсудим синтаксис С. В С принят необычный и разумный подход к синтаксису деклараций. Вместо описания типов с помощью особых конструкций, пишут выражение, включающее объявляемый элемент и указывают, какой тип будет у выражения. Таким образом,
объявляет x
как int
: выражение x
будет иметь тип int
.
В общем, чтобы понять, как записать тип новой переменной, напишите выражение, включающее эту переменную (которая будет иметь значение желаемого типа), а затем укажите тип слева от этой переменной.
Итак, следующие объявления
говорят, что p
это указатель на int
, потому что у *p
тип int
. А a
— это массив из int
, потому что a[3]
(игнорируя определённое значение индекса, которое прикреплено к размеру массива) имеет тип int
.
Что насчёт функций? Изначально объявление типов аргументов функций С выносились за скобки, вот так:
И снова мы видим, что main
это функция, потому что выражение main(argc, argv)
возвращает значение типа int
. В современном стиле мы бы написали:
но базовая структура одинакова.
Это неглупая идея для синтаксиса, хорошо работающая для простых типов, но может стать сбивающей с толку очень быстро. Рассмотрим известный пример объявления указателя на функцию. Следуйте правилам и у вас получится вот так:
Здесь fp
- это указатель на функцию, потому что если вы напишите выражение (*fp)(a, b)
, то вызовите функцию, возвращающую выражение с типом int
. А что, если один из аргументов fp
- это функция?
Читать становится труднее.
Конечно, можно убрать имена параметров при объявлении функции, и тогда main
может быть объявлена так:
Вспомним, что argv объявляется таким образом:
Так что вы опускаете имя из середины объявления, объявляя его тип. тем не менее, не так очевидно, что вы объявляете что-то имеющее тип char *[]
, размещая его имя в середине.
И посмотрите, что происходит с объявлением fp
, если у вас нет именованных параметров:
Кроме того, не так очевидно, где в коде разместить имя.
Вообще не понятно, что это объявление указателя на функцию. И что, если возвращаемый тип - это указатель на функцию?
Тут ещё более трудно разглядеть, что объявляется fp
.
Можно представить больше примеров, но уже приведённые должны показать некоторые трудности, которые даёт синтаксис объявлений в языке С.
Есть ещё один момент, о котором надо сказать. Из-за того, что синтаксис объявлений одинаков, может быть тяжело распарсить выражения с типами в середине. Поэтому, например, тип, к которому приводят выражение, всегда берётся в скобки, как в этом примере:
Синтаксис Go
Языки, не относящиеся к С-подобным, обычно используют определённый синтаксис типов в объявлениях. Хотя это и отдельная тема, имя, обычно, идёт первым, часто сопровождаясь двоеточием. Таким образом, наши примеры ниже станут чем-то вроде (псевдокод, который доступно покажет пример):
Эти объявления понятны, ведь вы читаете их слева направо. Go взял кое-что себе отсюда, но в интересах краткости двоеточие удаляется, как и удаляются некоторые из ключевых слов.
Нет прямой зависимости между тем, как выглядит [3]int
и как его использовать в выражении (мы ещё дойдём до указателей в следующем разделе). Вы приобритаете ясность за счёт отдельной конструкции.
Теперь рассмотрим функции. Давайте перепишем объявление функции main
, каким оно было бы в Go, хоть настоящая функция main
в Go и не принимает аргументов:
Внешне это не сильно отличается от C, кроме замены char на массив строк, но он хорошо читается слева направо: функция main принимает int и slice из strings и возвращает int.
Опустите имена параметров и всё будет так же ясно - они всегда шли первыми и путаницы не будет.
Одно из достоинств этого стиля “слева-направо” - это насколько хорошо он работает по мере усложнения типов. Вот объявление переменной-функции (аналог указателя на функцию в C):
Или если f
возвращает функцию:
Это все еще отчётливо читается слева направо, и всегда понятно, какое имя было объявлено - оно стоит на первом месте.
Различие между синтаксисом типов и выражений позволяет легко писать и вызывать замыкания в Go:
Указатели
Указатели являются исключением, которое подтверждает правило. Обратите внимание, что в массивах и слайсах, например, синтаксис для типов в Go подразумевает наличие скобок в левой части от типа, а синтаксис выражений их ставит в правой части выражения:
Ради схожести указатели в Go используют обозначение *
из С, но мы не cмогли заставить себя сделать подобный поворот для типов указателей. Таким образом, указатели работают так:
У нас бы не получилось сделать
потому что постфикс *
конфликтовал бы с умножением. Мы смогли бы использовать ^
из Pascal, например:
и, возможно, нам следовало (и мы бы выбрали другой оператор для xor
), потому что префикс *
в типах и выражениях вместе всё усложняет. Скажем, хоть и можно написать вот так
для преобразования, нужно брать в скобки тип, если он начинается с *
:
Если бы мы были готовы отказаться от *
как от части синтаксиса указателей, эти скобки были бы не нужны.
В общем, синтаксис Go для указателей связан со знакомой структурой в С, но эти связи означают, что мы не можем полностью отказаться от использования круглых скобок, чтобы различать типы и выражения в грамматике.
В целом, однако, мы считаем, что синтаксис типов в Go понять легче, чем в C, особенно, когда вещи становятся сложными.
Объявления в Go читаются слева направо. Выяснилось, что в C они читаются по спирали! Почитайте “Clockwise/Spiral Rule” от Дэвида Андерсона.