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 и линтеры используют его.

Типовые вопросы

  1. Почему foo.Bar доступен, а foo.bar нет?
    • Go определяет экспорт лексически: первая буква идентификатора должна быть прописной Unicode. Это не зависит от файла или модуля.
  2. Имя пакета и import path — одно и то же?
    • Нет. Path адресует пакет в модуле, а декларация package определяет имя квалификатора; они обычно похожи, но могут различаться.
  3. Когда нужен alias импорта?
    • При конфликте одинаковых имён либо когда имя пакета неясно. Alias не должен маскировать плохую структуру зависимостей.
  4. Что гарантирует internal?
    • Компилятор не позволит импортировать пакет извне разрешённого дерева. Он не делает данные секретными и не заменяет versioned public API.
  5. Зачем избегать init?
    • Он усложняет порядок запуска, тесты и подмену зависимостей. Явная инициализация делает ресурс и ошибки видимыми.
  6. Почему 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

Источники