Go: пакеты, видимость и именование
Пакет — единица компиляции и область имён Go: все .go-файлы одной директории обычно объявляют один package. Его import path определяется модулем и каталогом, а имя, используемое в коде, задаёт package в исходниках. Экспорт в Go намеренно прост: идентификатор, начинающийся с прописной буквы Unicode, доступен другому пакету; со строчной — только внутри своего пакета.
Зачем это на интервью
По структуре пакетов видно, умеет ли кандидат строить границы зависимости, публичный API и тестируемый код. Вопросы обычно проверяют разницу между именем пакета и import path, правило экспорта, роль internal и способность назвать API без Java-подобных повторов.
Минимум для E4
- Объяснить, что пакет — директория исходников с общим
package, а модуль задаёт набор версий и import paths. - Отличать
example.com/acme/paymentsот имениpaymentsвpackage payments. - Применять экспорт по первой букве, не рассчитывая на расположение файла или комментарий.
- Называть пакет коротким существительным в нижнем регистре без
_, а экспортируемый API — сCamelCase.
package invoice
// Number — публичный тип: его можно использовать как invoice.Number.
type Number string
// parseNumber остаётся деталью реализации пакета.
func parseNumber(raw string) Number { return Number(raw) }В потребителе импорт привязывается к имени пакета, а не к последнему сегменту пути:
package main
import inv "example.com/acme/billing/invoice"
func main() {
var n inv.Number = "A-42"
_ = n
}Углубление для E5/Senior
Публичная поверхность пакета — контракт совместимости. Добавление экспортируемой функции обычно обратно совместимо, но изменение сигнатуры, семантики нулевого значения или удаление символа ломает потребителей. Поэтому E5 сначала формулирует маленький контракт, прячет реализацию за неэкспортируемыми типами и возвращает интерфейс только когда он нужен потребителю, а не «на всякий случай».
Каталог internal ограничивает импорт на уровне компилятора: пакет a/b/internal/x могут импортировать только пакеты из дерева a/b. Это полезно для защиты деталей, но не заменяет хорошую архитектуру. Пакет cmd/foo обычно содержит точку входа конкретной программы; библиотечная логика живёт отдельно. Не следует делать один гигантский utils: он скрывает связность и превращается в неявный API.
Пакетные переменные и init создают неявный порядок и глобальное состояние. Инициализация импортов идёт до импортирующего пакета, а порядок между независимыми пакетами не должен быть частью дизайна. Для конфигурации и ресурсов предпочтительнее явные конструкторы и передача зависимостей.
Ключевые понятия
- Модуль — дерево пакетов с
go.mod; module path образует начало import path. - Пакет — набор файлов одной директории и области имён; тесты могут быть в
package pили внешнемpackage p_test. - Экспорт — доступность идентификатора из другого пакета по первой букве имени.
- Import path — уникальный путь зависимости, например
example.com/acme/service/user. - Package name — локальное имя квалификатора; alias допустим при конфликте или для ясности, но не для косметики.
internal— ограничение области импорта поддерева, проверяемое инструментами Go.- Документируемый API — экспортируемые сущности должны иметь комментарий, начинающийся с их имени;
go docи линтеры используют его.
Типовые вопросы
- Почему
foo.Barдоступен, аfoo.barнет?- Go определяет экспорт лексически: первая буква идентификатора должна быть прописной Unicode. Это не зависит от файла или модуля.
- Имя пакета и import path — одно и то же?
- Нет. Path адресует пакет в модуле, а декларация
packageопределяет имя квалификатора; они обычно похожи, но могут различаться.
- Нет. Path адресует пакет в модуле, а декларация
- Когда нужен alias импорта?
- При конфликте одинаковых имён либо когда имя пакета неясно. Alias не должен маскировать плохую структуру зависимостей.
- Что гарантирует
internal?- Компилятор не позволит импортировать пакет извне разрешённого дерева. Он не делает данные секретными и не заменяет versioned public API.
- Зачем избегать
init?- Он усложняет порядок запуска, тесты и подмену зависимостей. Явная инициализация делает ресурс и ошибки видимыми.
- Почему
package util— тревожный сигнал?- Название не говорит о предметной ответственности; пакет часто собирает несвязанный код и формирует неудобную циклическую зависимость.
Практика
- Создайте модуль
example.com/ledgerс пакетамиmoney,internal/formatиcmd/ledger. Готово:go test ./...проходит, а пакет вне дереваledgerне может импортироватьinternal/format. - Выделите из «utils» пакет с одной предметной ответственностью. Готово: у него есть короткое имя, отсутствуют циклические импорты, а экспортируемый API документирован.
- Напишите внешний тест
package money_test. Готово: тест использует только экспортируемые символы и не зависит от деталей реализации.
Частые ошибки и ловушки
- Путать директорию с import path и менять
packageради структуры URL. - Экспортировать поля структуры, хотя инвариант требует конструктора или метода.
- Давать пакетам имена
common,helpers,utilsбез предметной границы. - Создавать цикл импорта вместо инверсии зависимости через маленький интерфейс в пакете-потребителе.
- Использовать
initдля сетевых подключений, чтения окружения или регистрации, которую трудно отключить в тестах.
Связанные темы
Go · Основы языка Go · Структуры, embedding и теги · Модули, импорты и tooling