Статьи

Golang Internals (часть 1): основные концепции и структура проекта

[Это сообщение в блоге написано Сергеем Матюкевичем. Другие части серии: Часть 1 | Часть 2 | Часть 3 ]

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

  1. Какова структура исходного кода Go? 
  2. Как работает компилятор Go? 
  3.  Какова основная структура дерева узлов в Go?

Начиная

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

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

Прежде чем мы начнем, нам, безусловно, нужна наша собственная копия исходных файлов Go. Там нет ничего особенного в получении их. Просто выполните:

git clone https://github.com/golang/go

Обратите внимание, что код в основной ветке постоянно меняется, поэтому я использую ветку release-branch.go1.4 в этой записи блога.

Понимание структуры проекта

Если вы посмотрите на папку / src репозитория Go, вы увидите много папок. Большинство из них содержат исходные файлы стандартной библиотеки Go. Стандартные соглашения об именах всегда применяются здесь, поэтому каждый пакет находится внутри папки с именем, которое напрямую соответствует имени пакета. Помимо стандартной библиотеки, есть много других вещей. На мой взгляд, наиболее важными и полезными папками являются:

скоросшиватель Описание
/ SRC / CMD / Содержит различные инструменты командной строки.
/ SRC / ЦМД / пойти / Содержит исходные файлы инструмента Go, который загружает и создает исходные файлы Go и устанавливает пакеты. При этом он собирает все исходные файлы и обращается к инструментам командной строки компоновщика Go и компилятора Go.
/ SRC / CMD / расстояние / Содержит инструмент, отвечающий за сборку всех других инструментов командной строки и всех пакетов из стандартной библиотеки. Вы можете проанализировать его исходный код, чтобы понять, какие библиотеки используются в каждом конкретном инструменте или пакете.
/ SRC / CMD / дс / Это независимая от архитектуры часть компилятора Go.
/ SRC / CMD / ЛД / Архитектурно-независимая часть компоновщика Go. Части, зависящие от архитектуры, находятся в папке с постфиксом «l», в котором используются те же соглашения об именах, что и в компиляторе.
/ src / cmd / 5a / , 6a, 8a и 9a Здесь вы можете найти компиляторы Go для различных архитектур. Ассемблер Go — это форма языка ассемблера, которая не отображается точно на ассемблер базовой машины. Вместо этого для каждой архитектуры существует отдельный компилятор, который переводит ассемблер Go в ассемблер машины. Вы можете найти более подробную информацию здесь .
/ src / lib9 / , / src / libbio , / src / liblink Различные библиотеки, которые используются внутри компилятора, компоновщика и пакета времени выполнения.
/ SRC / среда / Самый важный пакет Go, который косвенно входит во все программы. Он содержит все функциональные возможности времени выполнения, такие как управление памятью, сборка мусора, создание подпрограмм и т. Д.

 

Внутри компилятора Go

Как я уже говорил выше, независимая от архитектуры часть компилятора Go находится в   папке / src / cmd / gc / . Точка входа находится в файле lex.c. Помимо некоторых общих вещей, таких как синтаксический анализ аргументов командной строки, компилятор делает следующее:

  1. Инициализирует некоторые общие структуры данных.

  2. Перебирает все предоставленные файлы Go и вызывает метод yyparse для каждого файла. Это приводит к фактическому анализу. Компилятор Go использует Bison в качестве генератора парсера. Грамматика для языка полностью описана в файле go.y (более подробно об этом позже). В результате на этом этапе создается полное дерево разбора, где каждый узел представляет элемент скомпилированной программы.

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

  4. Выполняет фактическую компиляцию после того, как дерево разбора завершено. Узлы переводятся в ассемблерный код.

  5. Создает объектный файл, который содержит сгенерированный код сборки с некоторыми дополнительными структурами данных, такими как таблица символов, которая генерируется и записывается на диск.

Погружение в грамматику го

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

xfndcl:
     LFUNC fndcl fnbody

fndcl:
     sym '(' oarg_type_list_ocomma ')' fnres
| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres

В этом объявлении определены узлы xfndcl и fundcl . Узел fundcl может быть в одной из двух форм. Первая форма соответствует следующей языковой конструкции:

somefunction(x int, y int) int

и второй к этой языковой конструкции:

(t *SomeType) somefunction(x int, y int) int

Узел xfndcl состоит из ключевого слова func , которое хранится в константе LFUNC , за которым следуют узлы fndcl и fnbody .

Важной особенностью грамматики Bison (или Yacc) является то, что она позволяет размещать произвольный код C рядом с каждым определением узла. Код выполняется каждый раз, когда в исходном коде найдено соответствие для этого определения узла. Здесь вы можете ссылаться на результирующий узел как $$, а на дочерние узлы — как $ 1 , $ 2 ,…

Это легче понять на примере. Обратите внимание, что следующий код является сокращенной версией фактического кода.

fndcl:
      sym '(' oarg_type_list_ocomma ')' fnres
        {
          t = nod(OTFUNC, N, N);
          t->list = $3;
          t->rlist = $5;

          $ = nod(ODCLFUNC, N, N);
          $->nname = newname($1);
          $->nname->ntype = t;
          declare($->nname, PFUNC);
      }
| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres

Сначала создается новый узел, который содержит информацию о типе для объявления функции. Список аргументов $ 3 и список результатов $ 5 ссылаются на этот узел. Затем создается узел результата $$ . Он хранит имя функции и тип узла. Как видите, не может быть прямого соответствия между определениями в файле go.y и структурой узлов.

Понимание узлов

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

Поле структуры узла Описание
оп Узел работы. У каждого узла есть это поле. Он отличает разные виды узлов друг от друга. В нашем предыдущем примере это были OTFUNC (функция типа операции) и ODCLFUNC (функция объявления операции).
тип Это ссылка на другую структуру с информацией о типе для узлов, которые имеют информацию о типе (для некоторых узлов нет типов, например, операторов потока управления, таких как if , switch или for ).
вал Это поле содержит фактические значения для узлов, которые представляют литералы.

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

Читать все части серии: Часть 1 | Часть 2 | Часть 3

Об авторе: Сергей Матюкевич — облачный инженер и разработчик Go в Altoros . С более чем 6-летним опытом в разработке программного обеспечения, он является экспертом в области облачной автоматизации и проектирования архитектур для сложных облачных систем. Активный участник сообщества Go, Сергей часто участвует в проектах с открытым исходным кодом, таких как Ubuntu и Juju Charms.