Урок курса
Собственные пакеты: структура папок, exported и unexported имена
Golang с нуляДо этого момента весь код жил в одном файле или в одной папке с пакетом main. Как только проект растёт — появляются вспомогательные функции, которые хочется переиспользовать — нужна другая структура. Go решает это очень конкретно: одна папка = один пакет.
Структура папок модуля и объявление пакетов main и mathutil
Создадим папку проекта и инициализируем модуль:
mkdir myapp && cd myapp go mod init example.com/myapp
Команда go mod init не создаёт никаких вложенных папок с именем модуля — она просто записывает module path в файл go.mod текущего каталога. Физическая структура на диске выглядит так:
myapp/
├── go.mod
├── main.go
└── mathutil/
└── mathutil.go
А содержимое go.mod — вот так:
module example.com/myapp go 1.21
example.com/myapp — это module path: строка-идентификатор модуля, которую Go использует при импорте пакетов. Она не обязана совпадать с именем папки на диске; папка называется myapp, а module path — example.com/myapp.
Файл main.go начинается с package main — это точка входа. Файл mathutil/mathutil.go начинается с package mathutil. Это всё, что требуется от каждого файла: объявить, к какому пакету он принадлежит.
Главное правило: все .go-файлы в одной папке должны объявлять одно и то же имя пакета. Попытка смешать package mathutil и package util в одной папке приведёт к ошибке компиляции. Компилятор считает папку единицей: он компилирует всё содержимое сразу и требует единого package-объявления.
Имя папки и имя пакета принято делать одинаковыми — mathutil/ содержит package mathutil. Технически компилятор не требует этого совпадения, но весь инструментарий Go (включая go doc и большинство IDE) рассчитывает именно на такое соответствие.
Если хочется добавить ещё одну группу функций — создаём ещё одну подпапку со своим .go-файлом и своим package-объявлением. Глубина вложенности не ограничена, но каждая папка всё равно остаётся одним пакетом.
Exported и unexported имена: правило заглавной буквы и ошибка компилятора при нарушении
Go не использует ключевые слова вроде public или private. Видимость имени определяется первой буквой его написания — и это единственное правило.
Идентификатор начинается с заглавной буквы → он exported, то есть виден из любого другого пакета. Начинается со строчной → unexported, виден только внутри того пакета, где объявлен.
Добавим в mathutil/mathutil.go две функции:
package mathutil
// Add — exported: доступна снаружи пакета
func Add(a, b int) int {
return a + b
}
// subtract — unexported: доступна только внутри mathutil
func subtract(a, b int) int {
return a - b
}
Теперь попробуем вызвать subtract из main:
package main
import "example.com/myapp/mathutil"
func main() {
mathutil.subtract(5, 3) // это не скомпилируется
}
Компилятор вернёт:
./main.go:6:2: cannot refer to unexported name mathutil.subtract
Это не предупреждение — это ошибка, программа не соберётся. Исправление ровно одно: переименовать subtract в Subtract в файле mathutil/mathutil.go. Никаких аннотаций, никаких модификаторов доступа — только заглавная буква.
Правило распространяется на всё: функции, переменные, константы, типы. Если Pi начинается с заглавной — её можно импортировать, если pi — нельзя. Это делает видимость немедленно читаемой прямо из исходника, без необходимости искать объявление в отдельном месте.
Импорт собственного пакета по пути модуля, блок import и циклическая зависимость
Путь импорта собственного пакета строится из двух частей: имя модуля из go.mod плюс путь подпапки относительно корня модуля. Если в go.mod написано module example.com/myapp, а пакет лежит в папке mathutil/, то импорт выглядит так:
import "example.com/myapp/mathutil"
После этого к exported-именам обращаются через имя пакета: mathutil.Add(2, 3). Имя, которое используется в коде — это именно package-имя из файла, не имя папки и не последний сегмент пути (хотя обычно они совпадают).
Когда нужно импортировать несколько пакетов, их группируют в блок:
import (
"fmt"
"example.com/myapp/mathutil"
)
Пустая строка между стандартными и собственными пакетами — общепринятое соглашение, goimports расставляет её автоматически. Если импортированный пакет нигде не используется, компилятор отказывается собирать программу — неиспользованный импорт запрещён.
Циклическая зависимость. Представим, что пакет api импортирует пакет storage, а storage решает импортировать api — например, чтобы использовать какой-то тип оттуда. Go-компилятор немедленно останавливается:
import cycle not allowed
Циклы запрещены полностью, без исключений. Решение зависит от причины цикла.
Если цикл возник из-за общего типа или константы, которая нужна обоим пакетам, — такой код выносят в третий пакет, например models, который не импортирует ни api, ни storage. Тогда оба пакета импортируют только models, и цикл исчезает:
api → models
↑
storage →
Этот приём работает именно тогда, когда общий код действительно независим и имеет смысл как отдельная единица.
В других ситуациях решение иное: иногда стоит пересмотреть направление зависимости — перенести ответственность в один из пакетов, чтобы второй больше не нуждался в обратном импорте. Иногда два тесно связанных пакета имеет смысл объединить в один. Универсального рецепта нет: цикл — это сигнал, что границы между пакетами стоит пересмотреть.
Попробуйте решить
В пакете geometry объявлены: func Area(r float64) float64 { ... } и func perimeter(r float64) float64 { ... }. Из пакета main написано geometry.perimeter(5.0). Что произойдёт при компиляции?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
