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

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

Rob Pike, 2012

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

5. Ввод Go

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

Хотя до сих пор обсуждение было сосредоточено на зависимостях, есть много других вопросов, которые требуют внимания. Основные соображения для любого языка, чтобы преуспеть в этом контексте:

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

На этом фоне давайте посмотрим на дизайн Go с точки зрения разработки программного обеспечения.

6. Зависимости в Go

Так как мы подробно рассмотрели зависимости в C и C++, хорошее место для начала нашего тура - посмотреть, как Go их обрабатывает. Зависимости определяются синтаксически и семантически языком. Они явные, понятные и "вычислимые", то есть простые в написании инструментов для анализа.

Синтаксис заключается в том, что после предложения package (тема следующего раздела) каждый исходный файл может иметь один или несколько утверждений импорта, включающих ключевое слово import и строковую константу, идентифицирующую пакет, который будет импортирован в этот исходный файл (только):

import "encoding/json"

Первый шаг к масштабированию Go с точки зрения зависимостей состоит в том, что язык определяет, что неиспользуемые зависимости являются ошибкой времени компиляции (не предупреждением, ошибкой). Если исходный файл импортирует пакет, который он не использует, программа не будет компилироваться. Это гарантирует по построению, что дерево зависимостей для любой программы Go является точным, что оно не имеет посторонних краев. Это, в свою очередь, гарантирует, что при сборке программы не будет скомпилирован дополнительный код, что минимизирует время компиляции.

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

  • пакет А импортирует пакет В
  • пакет B импортирует пакет C
  • пакет А не импортирует пакет С

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

Чтобы собрать эту программу, сначала компилируется C; зависимые пакеты должны быть собраны до пакетов, которые зависят от них. Затем B компилируется; наконец, A компилируется, и затем программа может быть связана.

Когда A компилируется, компилятор читает объектный файл для B, а не его исходный код. Этот объектный файл для B содержит всю информацию о типе, необходимую для выполнения компилятором

import "B"

пункта в исходном коде для A. Эта информация включает в себя любую информацию о C, которая понадобится клиентам B во время компиляции. Другими словами, когда B компилируется, сгенерированный объектный файл содержит информацию о типе для всех зависимостей B, которые влияют на открытый интерфейс B.

Этот дизайн имеет тот важный эффект, что когда компилятор выполняет утверждение import, он открывает ровно один файл, объектный файл, идентифицируемый строкой в ​​утверждении import. Это, конечно, напоминает подход Plan 9 C (в отличие от ANSI C) к управлению зависимостями, за исключением того, что компилятор записывает файл заголовка при компиляции исходного файла Go. Однако этот процесс более автоматический и даже более эффективный, чем в Plan 9 C: данные, считываемые при оценке импорта, представляют собой просто "экспортированные" данные, а не общий исходный код программы. Влияние на общее время компиляции может быть огромным и масштабироваться по мере роста базы кода. Время на выполнение графа зависимостей и, следовательно, на компиляцию может быть экспоненциально меньше, чем в модели "включая включаемый файл" C и C++.

Стоит отметить, что этот общий подход к управлению зависимостями не оригинален; идеи восходят к 1970-м годам и распространяются через такие языки, как Modula-2 и Ada. В семействе C Java имеет элементы этого подхода.

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

Такой подход к управлению зависимостями является единственной основной причиной того, что компиляции Go выполняются быстрее, чем компиляции на C или C++. Другим фактором является то, что Go помещает данные экспорта в объектный файл; некоторые языки требуют, чтобы автор писал или компилятор генерировал второй файл с этой информацией. Это вдвое больше файлов, чтобы открыть. В Go есть только один файл, который нужно открыть для импорта пакета. Кроме того, подход с одним файлом означает, что данные экспорта (или заголовочный файл в C/C++) никогда не могут устареть относительно объектного файла.

Для записи, мы измерили компиляцию большой программы Google, написанной на Go, чтобы увидеть, как разветвление исходного кода сравнивается с анализом C++, сделанным ранее. Мы обнаружили, что это примерно 40Х, что в пятьдесят раз лучше, чем в C++ (а также проще и, следовательно, быстрее в обработке), но все же больше, чем мы ожидали. Для этого есть две причины. Во-первых, мы обнаружили ошибку: компилятор Go генерировал значительное количество данных в разделе экспорта, которые там не обязательно должны быть. Во-вторых, в данных экспорта используется подробная кодировка, которая может быть улучшена. Мы планируем решить эти проблемы.

Тем не менее, выполнение задачи в пятьдесят раз превращает минуты в секунды, перерывы на кофе превращаются в интерактивные сборки.

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

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

При разработке стандартной библиотеки большие усилия были потрачены на контроль зависимостей. Может быть лучше скопировать небольшой код, чем использовать большую библиотеку для одной функции. (Тест в сборке системы жалуется, если возникают новые основные зависимости.) Гигиена зависимостей препятствует повторному использованию кода. Одним из примеров этого на практике является то, что (низкоуровневый) net пакет имеет свою собственную процедуру преобразования целочисленных значений в десятичные, чтобы избежать зависимости от большого и имеющего много зависимостей отформатированного пакета ввода-вывода. Другой заключается в том, что пакет преобразования строк strconv имеет частную реализацию определения 'печатаемых' символов, а не вытягивает большие таблицы классов символов Юникода; то, что strconv соблюдает стандарт Unicode, проверяется тестами пакета.

7. Пакеты

Проект системы пакетов Go объединяет некоторые свойства библиотек, пространств имен и модулей в единую конструкцию.

Каждый исходный файл Go, например "encoding/json/json.go", начинается с утверждения package, например:

package json

где json - это "имя пакета", простой идентификатор. Имена пакетов обычно лаконичны.

Чтобы использовать пакет, импортирующий исходный файл идентифицирует его по пути пакета в утверждении import. Значение "путь пакета" не определено языком, но на практике и условно это разделенный слешем путь к каталогу с исходным файлом пакета в репозитории:

import "encoding/json"

Затем имя пакета (в отличие от пути) используется для определения элементов из пакета в импортируещем исходном файле:

var dec = json.NewDecoder(reader)

Этот дизайн обеспечивает ясность. Всегда можно сказать, является ли имя локальным для пакета по его синтаксису: Name vs. pkg.Name (Подробнее об этом позже.)

В нашем примере путь к пакету - "encoding/json", а имя пакета - "json". Вне стандартного репозитория соглашение должно помещать имя проекта или компании в корень пространства имен:

import "google/base/go/log"

Важно понимать, что пути к пакетам уникальны, но для имен пакетов таких требований нет. Путь должен однозначно идентифицировать импортируемый пакет, а имя - это соглашение о том, как клиенты пакета могут ссылаться на его содержимое. Имя пакета не обязательно должно быть уникальным и может быть переопределено в каждом исходном файле импорта путем предоставления локального идентификатора в утверждении import. Следующие два оба импортируют эталонные пакеты, которые называют себя package log, но для импорта их в один исходный файл один из них должен быть (локально) переименован:

import "log"                          // Standard package
import googlelog "google/base/go/log" // Google-specific package

Каждая компания может иметь свой собственный package log, но нет необходимости делать имя пакета уникальным. Скорее наоборот: стиль Go предполагает, что имена пакетов должны быть короткими, четкими и очевидными, а не беспокоиться о коллизиях.

Другой пример: в базе кода Google много server пакетов.

8. Удаленные пакеты

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

Вот как можно использовать пакет doozer с github. Команда go get использует инструмент go build для извлечения репозитория с сайта и его установки. После установки его можно импортировать и использовать как любой обычный пакет.

$ go get github.com/4ad/doozer // Shell команда для получения пакета

import "github.com/4ad/doozer" // import утверждение doozer клиентом

var client doozer.Conn         // использование клиентом пакета

Стоит отметить, что команда go get рекурсивно загружает зависимости, свойство стало возможным только потому, что зависимости явные. Кроме того, выделение пространства путей импорта делегируется URL-адресам, что делает децентрализованным и, следовательно, масштабируемым именование пакетов в отличие от централизованных реестров, используемых другими языками.


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


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

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