Skip to content

Latest commit

 

History

History
130 lines (91 loc) · 18.2 KB

05-modules.adoc

File metadata and controls

130 lines (91 loc) · 18.2 KB

Пакеты и модули

Помните, что первой строкой каждого файла с кодом на Go является строка вида

package main

Эта строка декларирует имя пакета. Весь код, лежащий в одной папке должен принадлежать одному пакету. Это означает, что во всех файлах внутри папки должна быть одинаковая строка объявления пакета. Кроме файлов с тестами, но об этом немного позже. Внутри пакета все функции, типы и глобальные переменные и константы доступны в любом файле без префиксов как в Erlang, то есть нет нужды импортировать рядом лежащий файл, как в NodeJS или Python. С одной стороны это удобно, с другой стороны с непривычки может путать, но при наличии редакторов с поддержкой перехода к определению к этому достаточно легко привыкнуть и приспособиться.

Стандартной практикой является называть пакет именем папки, в которой он лежит. Это, конечно, не относится к пакетам main, которые являются точками входа и вряд ли будут импортироваться в другие пакеты. К именам пакетов, также как и к переменным, golint предъявляет требования соответствовать camelCase. Но при учёте того, что некоторые файловые системы нечуствительны к регистру, лучше вообще называть пакеты строчными буквами, не прибегая к другим символам.

Несмотря на короткое название пакета, использования его в другом пакете не так очевидно. Для того, чтобы с этим разобраться необходимо перейти к понятию модуля.

Модули

Note
Язык проектировался и развивается силами разработчиков из компании Google, что накладывает некоторые отпечатки. Одним из таких отпечатков является то, что внутри компании используется монорепозиторий, то есть весь код всех отделов компании лежит в одном большом репозитории. Отчасти из-за этого история модулей в языке получилась такая, какая получилась.

Вначале был $GOPATH

С самой первой версии для работы с языком было необходимо настроить переменную окружения $GOPATH. Принцип работы этой переменной вызывало максимальное непонимание для новичков в языке, так что в итоге в версии 1.8 для этой переменной ввели значение по умолчанию (~/go), а начиная с версии 1.12 можно работать вообще не зная ничего об этой переменной. Однако, давайте всё же разберёмся — как она работает.

Переменная $GOPATH должна содержать путь к директории, в которой будет хранится информация обо всех сторонних пакетах, в том числе и разрабатываемых. Внутри этой директории будет созданы три директории: src, pkg и bin. В последнюю будут автоматически попадать скомпилированные исполняемые файлы, при установке пакетов, собирающихся в таковые. Например, если установить пакет stringer, из него будет собран исполняемый файл stringer. В директории pkg будет собираться кеш версий различных библиотек, а также промежуточные артефакты компиляций. Наконец, директория src содержит исходный код пакетов и модулей. Начиная с версии 1.13, установка пакета сохраняет его исходные коды только в pkg, так что в src остались только разрабатываемые вами исходные коды. И полное имя пакета для импорта совпадает с путём до пакета относительно директории src. Таким образом, если в директории src создать директорию foo/bar, в которую положить go-файлы, то для импорта этого пакета в другом пакете в директиве import будет необходимо прописать строку "foo/bar".

Сторонние библиотеки

Для установки сторонних библиотек с самой первой версии существует команда go get <имя пакета>. В качестве источников пакетов используются git-репозитории, например, https://github.com или https://golang.org. Собственно для установки того же пакета stringer в консоли необходимо выполнить

go get golang.org/x/tools/cmd/stringer

Эта команда выполнит следующее:

  1. создаст директорию $GOPATH/src/golang.org/x/tools/cmd/stringer;

  2. склонирует туда дефолтную ветку этого репозитория;

  3. если в пакете есть подпакет main, скомпилирует исполняемый файл и положит его в $GOPATH/bin;

Для обновления можно использовать дополнительный флаг -u. А после внесения изменений можно пересобрать исполняемый файл с помощью команды

go install golang.org/x/tools/cmd/stringer

При этом src будет лежать полноценный git-репозиторий, как есть.

Если одна библиотека использовала другую, то при установке устанавливались и зависимости. Конечно без всякого версионирования, просто последний коммит из master-ветки. Но что хорошо для монорепозитория, плохо для большого распределённого сообщества. Так в версии 1.5 появилась поддержка специальной папки vendor, содержащей в себе зависимости. Также правилом хорошего тона стало включение этой папки в репозитории. При сборке зависимость в первую очередь ищется в папках vendor рекурсивно от текущего пакета до $GOPATH/src, после чего ищется уже в $GOPATH/src. Но не смотря на поддержку этой папки во время компиляции, не было никакого официального тулинга для работы с содержимым этой папки. Так появились инструменты, разрабатываемые сообществом.

А потом пришли модули

В конце февраля 2018 года один из корневых разработчиков языка Расс Кокс выпустил серию статей и прототип того, что в последствии стало называться go-модули. Начиная с версии языка 1.10 поддержка модулей появилась в наборе команд go mod.

Модули позволяют версионировать библиотеки и их зависимости, а также, при необходимости управлять директорией vendor. Для того, чтобы создать модуль, достаточно в любой директории выполнить команду go mod init. После этого в директории появится файл go.mod, в котором будут описываться зависимости текущего модуля. Первой строкой в таком файле указано название модуля. Это название важно в том смысле, что работать с модулем можно за пределами $GOPATH. При разрешении зависимостей, если зависимость начинается с названия модуля, то она будет искаться относительно этого модуля.

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

v{major:\d+}.{minor:\d+}.{patch:\d+}[-.*]

Если в репозитории нет ни одного тэга, подходящего под это определение, то будет взята голова master-ветки. Информация об устанавливаемых версиях будет накапливаться в файле go.mod. При необходимости включения зависимости целиком можно выполнить команду go mod vendor, которая соберёт все зависимости с нужными версиями и скопирует их в директорию vendor.

Если две или более зависимости ссылаются на разные версии одной библиотеки, то для минорных версий будет выбрана максимальная из требуемых, а мажорные версии могут работать параллельно, используя суффиксы в импортах v2. Более подробно о разрешении зависимостей и других особенностях поведения модулей можно прочитать в серии статей Расса Кокса.

Публичные и приватные сущности

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

type User struct {
    ID       int
    Email    string
    password []byte
}

то для внешних пакетов этот тип будет доступен, а у объектов этого типа будут доступны поля ID и Email, поле password будет приватным.

Циклические зависимости

Одной из частых проблем, с которой можно столкнуться при разработке на Go — циклические зависимости. То есть пакет A импортирует пакет B, который импортирует пакет C, импортирующий в свою очередь пакет A. Циклы могут быть разной длинны, сути это не меняет. Компилятор не может разрешить такой граф и соответственно скомпилироать код. Для разрешения таких зависимостей чаще всего приходится пересматривать архитектуру проекта, используя такие подходы как чистая архитектура, предложенная Робертом Мартином или восьмиугольную, луковую, любую другую позволяющую разделить абстракции. Взаимодействие же между уровнями абстракции строить на стандартных типах или на общих чистых сущностях (моделях), а также прибегая к интерфейсам. Один из вариантов организации проекта предложен в статье «Чистая архитектура на Go»[cag]. С другой стороны при использовании интерфейсов циклическая зависимость может возникнуть в тестах. Для разрешения таких зависимостей можно вынести тесты в отдельный модуль.

Тесты в отдельном пакете

Для тестов допустимо использовать имя пакета с добавлением суффикса _test, не перемещая их файлы в другую директорию. Это оказывается удобным для написания функциональных тестов по типу чёрного ящика. Тесты своего рода оказываются во внешнем пакете, поэтому в них доступны только публичные свойства и методы. Дополнительным плюсом оказывается, что примеры (функции Example…​ из тестовых файлов) будут более приближены к реальному использованию:

package sort_test

import (
    "fmt"

    "github.com/superSorter/sort"
)

func ExampleFlashSort() {
    a := []int{12, 22, 11, 0, 9}
    sort.FlashSort(a)
    fmt.Println(a)
    // Output: [0 9 11 12 22]
}

Соглашения

В сообществе сложилось несколько правил к именованию директорий и пакетов.

Первое правило поддерживается компилятором и тулингом. Пакеты располагаемые внутри директории internal доступны только в родительском пакете. То есть, если у нас есть такая структура директорий:

foo/
  bar/
  internal/
    baz/
qux/

то пакет foo/internal/baz можно импортировать в пакетах foo и foo/bar, но нельзя в пакете qux. Это особенно полезно для того, чтобы не смущать такие инструменты как goimports. Если у вас есть 10 сервисов, в каждом из которых есть пакет model, то утилита goimports может подставить импорт из соседнего сервиса вместо того, чтобы использовать модели текущего. Если же пакеты моделей спрятать внутри internal, то вопрос о том откуда импортировать пакет не возникнет.

Остальные правила не поддерживаются тулингом или компилятором, однако хорошо закрепились в правилах хорошего тона. Для точек входа (пакетов main) использовать подкатологи cmd. Часто возникает необходимость уметь собирать из одного кода несколько артефактов, например, сервис и терминальный клиент к сервису или утилиту для миграций. Тогда структура проекта может выглядеть примерно так

module/
  cmd/
    service/main.go
    client/main.go
    migrate/main.go
  internal/
    ...
  go.mod

При разработке библиотеки иногда возникает необходимость вводить экспериментальные функции, которые работают нестабильно или их интерфейс может измениться. Такие функции принято выносить в пакет x или его подпакеты. Ярким примером такого подхода является стандартная библиотека Go. Так для стандартного пакета net есть пакет экспериментальных функций golang.org/x/net.