понедельник, 4 ноября 2019 г.

Golang в Google: дизайн языка на службе разработки программ, часть 3

Rob Pike, 2012

Продолжение, начало в части 1 и части 2.

9. Синтаксис

Синтаксис - это пользовательский интерфейс языка программирования. Хотя это оказывает ограниченное влияние на семантику языка, который, возможно, является наиболее важным компонентом, синтаксис определяет читабельность и, следовательно, ясность языка. Кроме того, синтаксис имеет решающее значение для инструментов: если язык трудно анализировать, автоматизированные инструменты трудно писать.

Таким образом, Go был разработан с ясностью и удобством и имеет чистый синтаксис. По сравнению с другими языками в семействе C его грамматика скромна по размеру: всего 25 ключевых слов (C99 - 37; C++ 11 - 84; цифры продолжают расти). Что еще более важно, грамматика является правильной и поэтому легко разбирается (в основном; есть несколько причуд, которые мы могли бы исправить, но не обнаружили достаточно рано). В отличие от C и Java и особенно C++, Go может быть проанализирован без информации о типе или таблицы символов; нет конкретного типа контекста. Грамматику легко разъяснить, и поэтому инструменты легко писать.

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

var fn func([]int) int
type T struct { a, b int }

по сравнению с C

int (*fn)(int[]);
struct T { int a, b; }

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

var buf *bytes.Buffer = bytes.NewBuffer(x) // явное
buf := bytes.NewBuffer(x)                  // производное

Синтаксис функции прост для простых функций. В этом примере объявляется функция Abs, которая принимает одну переменную x типа T и возвращает единственное значение float64:

func Abs(x T) float64

Метод - это просто функция со специальным параметром, его получателем, который можно передать функции, используя стандартную запись "точка". Синтаксис объявления метода помещает получателя в скобки перед именем функции. Вот та же самая функция, теперь как метод типа T:

func (x T) Abs() float64

А вот переменная (замыкание) с аргументом типа T; Go имеет функции первого класса и замыкания:

negAbs := func(x T) float64 { return -Abs(x) }

Наконец, в Go функции могут возвращать несколько значений. Распространенным случаем является возврат результата функции и значения ошибки в виде пары, например:

func ReadByte() (c byte, err error)

c, err := ReadByte()
if err != nil { ... }

Мы поговорим об ошибках позже.

В Go есть одна особенность - он ​​не поддерживает аргументы функций по умолчанию. Это было намеренное упрощение. Опыт подсказывает нам, что аргументы по умолчанию позволяют слишком легко исправлять недостатки проектирования API, добавляя больше аргументов, что приводит к слишком большому количеству аргументов с взаимодействиями, которые трудно распутать или даже понять. Отсутствие аргументов по умолчанию требует определения большего количества функций или методов, так как одна функция не может содержать весь интерфейс, но это приводит к более ясному API, который легче понять. Все эти функции также нуждаются в отдельных именах, что проясняет, какие комбинации существуют, а также стимулирует к большему размышлению о наименовании, критическом аспекте ясности и читабельности.

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

10. Именование

Go использует необычный подход к определению видимости идентификатора, возможность для клиента пакета использовать элемент, названный идентификатором. В отличие, например, от private и public ключевых слов, в Go само имя содержит информацию: регистр начальной буквы идентификатора определяет видимость. Если начальный символ является заглавной буквой, идентификатор экспортируется (общедоступный); в противном случае это не так:

  • заглавная буква: Name видно клиентам пакета
  • в противном случае: name (или _Name) не видно клиентам пакета

Это правило распространяется на переменные, типы, функции, методы, константы, поля - на все. Это все, что нужно сделать.

Это было непростое дизайнерское решение. Мы потратили более года, пытаясь определить нотацию, чтобы определить видимость идентификатора. Как только мы решили использовать регистр имени, мы вскоре поняли, что оно стало одним из самых важных свойств языка. В конце концов, имя - это то, что используют клиенты пакета; добавление видимости в имя, а не в его тип означает, что при взгляде на идентификатор всегда ясно, является ли оно частью общедоступного API. После использования Go некоторое время становится обременительным, когда вы возвращаетесь к другим языкам, которые требуют поиска декларации для обнаружения этой информации.

Результат, опять же, ясность: исходный текст программы просто выражает значение программиста.

Другое упрощение заключается в том, что Go имеет очень компактную иерархию областей видимости:

  • всеобъемлящая (universe) (предварительно объявленные идентификаторы, такие как int и string)
  • пакет (все исходные файлы пакета находятся в одной области видимости)
  • файл (только для импорта пакетов переименовывает; на практике это не очень важно)
  • функция (обычная)
  • блок (обычный)

Нет пространства для пространства имен, класса или другой оберточной конструкции. Имена происходят из очень немногих мест в Go, и все имена соответствуют одной и той же иерархии областей видимости: в любом заданном месте в исходном коде идентификатор обозначает ровно один языковой объект, независимо от того, как он используется. (Единственное исключение - метки утверждений, цели утверждений break и т.п.; они всегда имеют область видимость функции.)

Это имеет последствия для ясности. Обратите внимание, например, что методы объявляют явного получателя и что он должен использоваться для доступа к полям и методам типа. Там нет неявного this. То есть всегда пишут

rcvr.Field

(где rcvr - это любое имя, выбранное для переменной-получателя), поэтому все элементы типа всегда отображаются лексически связанными со значением типа-получателя. Точно так же квалификатор пакета всегда присутствует для импортированных имен; всегда пишут io.Reader, а не Reader. Это не только ясно, но и освобождает идентификатор Reader как полезное имя для использования в любом пакете. На самом деле в стандартной библиотеке есть несколько экспортируемых идентификаторов с именем Reader или Printf, но какой из них упоминается, всегда однозначно.

Наконец, эти правила объединяются, чтобы гарантировать, что кроме предопределенных имен верхнего уровня, таких как int, (первый компонент) каждое имя всегда объявляется в текущем пакете.

Кратко, имена локальные. В C, C++ или Java имя y может относиться к чему угодно. В Go y (или даже Y) всегда определяется внутри пакета, в то время как интерпретация x.Y ясна: найдите x локально, Y принадлежит ему.

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

Следует упомянуть еще один аспект именования: поиск метода всегда осуществляется только по имени, а не по сигнатуре (типу) метода. Другими словами, у одного типа никогда не может быть двух методов с одинаковым именем. Учитывая метод x.M, существует только один M, связанный с x. Опять же, это позволяет легко определить, к какому методу относится только имя. Это также упрощает реализацию вызова метода.

11. Семантика

Семантика операторов Go обычно C-подобна. Это компилируемый, статически типизированный, процедурный язык с указателями и так далее. По замыслу он должен чувствовать себя знакомым программистам, привыкшим к языкам в семействе С. При запуске нового языка важно, чтобы целевая аудитория могла быстро его выучить; укоренение Go в семействе C помогает молодым программистам, большинство из которых знают Java, JavaScript и, возможно, C, находить Go легким в освоении.

Тем не менее, Go вносит много небольших изменений в семантику C, в основном для обеспечения надежности. Они включают:

  • нет арифметики указателей
  • нет неявных числовых преобразований
  • границы массива всегда проверяются
  • нет псевдонимов типов (после типа X int, X и int являются разными типами, а не псевдонимами)
  • ++ и -- являются утверждениями, а не выражениями
  • присваивание не является выражением
  • законно (даже рекомендуется) брать адрес стековой переменной
  • и многое другое

Существуют и более значительные изменения, далеко идущие от традиционных моделей C, C++ и даже Java. К ним относятся лингвистическая поддержка для:

  • конкурентности
  • сборки мусора
  • типов интерфейсов
  • отражения (reflection)
  • переключателей типа

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


Читайте также:


Комментариев нет:

Отправить комментарий