Deep Engineering
Средний·Опубликовано·50 МИН

Пакеты в Go: видимость, init и структура проекта

Собеседование по пакетам идёт лестницей: как сделать имя приватным — что такое пакет и при чём тут каталог — зачем internal — в каком порядке выполняются init — зачем импорт с подчёркиванием — как разложить проект. Прогон отвечает на каждый шаг: переменная, объявленная первой, инициализируется последней; пакет alpha инициализируется раньше zeta, хотя main импортирует zeta первым; импорт из internal запрещает не компилятор, а команда go; а у одного и того же нарушения видимости два разных сообщения компилятора.

Полное техническое изложение

TL;DR

Пакет — это каталог исходных файлов с одним и тем же именем в строке package; он единица видимости и единица инициализации. Из другого пакета видны только имена, которые начинаются с заглавной буквы, — модификаторов доступа в Go нет. Импортируют пакет по пути (.../naming/strutil), а обращаются по имени из его строки package (textkit.Shout) — и это не одно и то же.

Порядок инициализации задаёт граф зависимостей, а не порядок в тексте. var total = price*qty, объявленная первой, инициализируется третьей — после price и qty. Функции init() вызываются после всех переменных пакета, по порядку файлов. Пакет инициализируется только после всех, кого он импортирует, и ровно один раз; среди готовых берётся первый по пути импорта — поэтому alpha в прогоне инициализирован раньше zeta, хотя main импортирует zeta первым.

Дальше — то, что отличает понимание от пересказа. Импорт из internal/ запрещает не компилятор и не спецификация, а команда go: use of internal package ... not allowed. cmd/ и pkg/ — соглашения, которых не проверяет никто. Экспортируемая функция может вернуть значение неэкспортируемого типа — вызвать его методы можно, назвать тип нельзя. У одного нарушения видимости два разных сообщения компилятора (наблюдение gc go1.24.7, а не правило языка), и какое выпадет, зависит от встраивания функций — подстановки тела функции в место вызова. Пустой импорт _ "image/png" нужен ради одного init, который регистрирует декодер, — без него image.Decode отвечает image: unknown format. А программа с одним fmt — это десятки пакетов, и все они проходят инициализацию до main.

Порог входа
Перед уроком достаточно понимать
  • как написать функцию, тип и переменную на уровне пакета;
  • что такое структура, её поля и метод с получателем — об этом урок о структурах;
  • что программа на Go начинается с функции main в пакете main;
  • как собрать и запустить программу командами go build и go run — об этом урок об инструментах go (там же — ограничения сборки, которые здесь упоминаются мимоходом).
  • Тесты _test.go и интерфейсы встретятся в паре мест; подробно о них — в уроках о тестах и об интерфейсах, они идут по линии позже.
Заранее знать не нужно
  • одно имя пакета на каталог, путь импорта против имени пакета, go list;
  • экспорт по первой букве, неэкспортируемый тип в экспортируемом API, internal/ и соглашения раскладки;
  • порядок инициализации переменных, файлов, init() и пакетов, цикл импортов, пустой импорт.

Что здесь на самом деле спрашивают

Лестница почти всегда такая:

  1. «Как в Go сделать имя приватным?» — разминка; ответ «с маленькой буквы» верен, но это половина правила.
  2. «Что такое пакет и как он связан с каталогом?» — проверяют, отличает ли кандидат путь импорта от имени пакета.
  3. «Зачем каталог internal?» — и кто именно запрещает такой импорт.
  4. «В каком порядке выполняются init?» — главный вопрос: переменные, файлы, пакеты.
  5. «Зачем импорт с подчёркиванием?» — драйверы database/sql, форматы image.
  6. «Как разложить проект?» — cmd/, internal/, pkg/: что из этого правило, а что привычка.

Стержень у лестницы один: у каждого правила о пакетах свой владелец — спецификация языка, компилятор, команда go или никто. Экспорт и порядок инициализации — язык. Одно имя на каталог и internal/ — команда go. cmd/ и pkg/ — соглашение. Сильный ответ начинается с того, кто проверяет правило.

База: пакет — каталог файлов с одним именем

Спецификация определяет пакет через файлы, а не через каталог, — и оставляет каталог на усмотрение реализации:

A set of files sharing the same PackageName form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory.

перевод

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

— Спецификация Go — Package clause
контракт языкаПравила экспорта, импорта и инициализации — спецификация языка; одно имя на каталог, internal/ и пропуск файлов на _ — документация команды go (go help packages, go help gopath). Прогон bench/gopkg/internals.go их иллюстрирует, а не устанавливает. Номера в заголовках блоков — номера разделов записи runs/internals.txt, поэтому в уроке они идут не подряд. Прогоны — go1.24.7 и go1.25.1, linux/amd64; сверено 14.10.2026. Вышедшие позже Go 1.26 и 1.27 урок не измерял.

Команда go этим правом пользуется: пакет — это каталог, и все .go-файлы в нём, которые входят в сборку, обязаны начинаться с одного и того же package. Два файла с package alpha и package beta в одном каталоге не собираются:

Вывод bench/gopkg/internals.go, раздел 1 (обе программы намеренно не собираются):

1) ПАКЕТ — ЭТО КАТАЛОГ: ОДНО ИМЯ НА КАТАЛОГ
───────────────────────────────────────────
go build ./twopkg   (alpha.go: package alpha, beta.go: package beta)
    found packages alpha (alpha.go) and beta (beta.go) in <cases>/twopkg
go build ./xtestname   (util.go: package util, helper.go: package util_test)
    found packages util_test (helper.go) and util (util.go) in <cases>/xtestname

Второй случай — подсказка к единственному исключению: имя с суффиксом _test в файле, который сам называется *_test.go. Такой файл — внешний тест, отдельный пакет в том же каталоге. В обычном файле то же имя — та же ошибка.

Внутри пакета видно всё — из любого файла, без импортов друг друга. Снаружи видно только экспортируемое. Этого достаточно для ответа уровня Junior: пакет — каталог .go-файлов с одним именем в строке package. Имена с заглавной буквы видны из других пакетов, остальные — только внутри пакета. Импортируют пакет по пути, обращаются — по имени пакета.

Всё это помещается в самый короткий полный пример: модуль из двух пакетов. Модуль — дерево пакетов под одним файлом go.mod, который задаёт префикс путей импорта; подробно — в уроке о модулях. Три файла bench/gopkg/cases/example можно скопировать в пустой каталог и запустить командой go run . рядом с go.mod.

Полный файл go.mod:

TEXT
module example.com/hello
 
go 1.24

Полный файл greet/greet.go:

GO
// Package greet is the second package of the lesson's full example.
package greet
 
import "fmt"
 
// calls is set by init before main starts.
var calls int
 
func init() { calls = 1 }
 
// Hello is exported: its name starts with an uppercase letter.
func Hello(name string) string {
	return fmt.Sprintf("%s, %s (init ran: %v)", prefix(), name, calls == 1)
}
 
// prefix is not exported: only files of package greet can call it.
func prefix() string { return "hello" }

Полный файл main.go:

GO
// Full example of the lesson "Packages in Go": two packages in one module.
//
// Run in this directory (next to go.mod): go run .
package main
 
import (
	"fmt"
 
	"example.com/hello/greet"
)
 
func main() {
	fmt.Println(greet.Hello("gopher"))
}

Вывод bench/gopkg/internals.go, раздел 14:

14) ПОЛНЫЙ ПРИМЕР: ДВА ПАКЕТА В ОДНОМ МОДУЛЕ
────────────────────────────────────────────
cd cases/example && go list ./...
    example.com/hello
    example.com/hello/greet
cd cases/example && go run .
    hello, gopher (init ran: true)

Путь импорта example.com/hello/greet — префикс модуля плюс каталог; обращение — greet.Hello, по имени пакета. init пакета greet выполнилась раньше main. А greet.prefix из main.go вызвать нельзя — имя с маленькой буквы; что именно ответит компилятор, показано в Механизме 2.

Одну таблицу стоит запомнить сразу — она отвечает на вопрос «кто это проверяет», с которого начинается половина следующих шагов:

правилокто его владелецчто будет при нарушении
экспорт по первой буквеспецификация, проверяет компиляторошибка компиляции
порядок инициализацииспецификация— (порядок просто такой)
цикл импортов, импорт без обращенийспецификацияошибка сборки
одно имя пакета на каталогкоманда gofound packages ...
internal/команда gouse of internal package ... not allowed
cmd/, pkg/никто — соглашениеничего

Механизм 1: путь импорта и имя пакета — разные вещи

Строка import называет путь — где пакет лежит. Обращение в коде идёт по имени — тому, что стоит в строке package импортированного пакета. Спецификация так и говорит: если имя при импорте не указано, берётся имя из строки package. Обычно путь кончается тем же словом, что и имя, — но это договорённость, а не правило. В прогоне каталог naming/strutil содержит package textkit:

Вывод bench/gopkg/internals.go, раздел 3 (wrongname намеренно не собирается; правильный вариант — строки выше него):

3) ПУТЬ ИМПОРТА И ИМЯ ПАКЕТА — РАЗНЫЕ ВЕЩИ
──────────────────────────────────────────
go list -f '{{.ImportPath}} -> package {{.Name}}' ./naming/strutil
    deepengineering/bench/gopkg/cases/naming/strutil  ->  package textkit
go run ./naming   (import ".../naming/strutil"; textkit.Shout("hello"))
    HELLO!
go run ./naming/alias   (tk ".../naming/strutil"; tk.Shout("alias"))
    ALIAS!
go build ./naming/wrongname   (strutil.Shout — по имени каталога)
    naming/wrongname/main.go:6:2: "deepengineering/bench/gopkg/cases/naming/strutil" imported as textkit and not used
    naming/wrongname/main.go:10:14: undefined: strutil

Последний случай — расхождение пути и имени на практике: читатель видит import ".../strutil", пишет strutil.Shout и получает две ошибки сразу. Поэтому имя пакета держат равным последнему элементу пути, а при расхождении пишут явный псевдоним: tk ".../strutil".

Что именно входит в пакет, решают не только строки package, но и имена файлов. Самый быстрый способ это увидеть — go list с шаблоном по полям структуры Package, которые описывает go help list:

Вывод bench/gopkg/internals.go, раздел 2:

2) ЧТО ВХОДИТ В ПАКЕТ: go list
──────────────────────────────
файлы в cases/listing:
    _draft.go api_test.go gen.go listing.go listing_linux.go listing_test.go listing_windows.go testdata/
GOOS=linux go list -f '…' ./listing
    Name=listing
    GoFiles=[listing.go listing_linux.go]
    IgnoredGoFiles=[gen.go listing_windows.go]
    TestGoFiles=[listing_test.go]
    XTestGoFiles=[api_test.go]

Семь файлов и каталог — а в сборку пакета под Linux попали два. listing_windows.go и gen.go (с //go:build ignore) отсеяли ограничения сборки — о них урок об инструментах go; с GOOS=windows тот же go list меняет _linux и _windows местами. gen.go объявляет package main, и ошибки found packages нет: правило «одно имя на каталог» касается только файлов, вошедших в сборку. А _draft.go и testdata/ не упомянуты нигде — в обоих лежит не Go, и go vet ./listing всё равно проходит. Команда go их не читает. По go help packages, каталоги и файлы на . и _ команда go пропускает, as are directories named "testdata" (как и каталоги с именем testdata).

Механизм 2: экспорт решает первая буква — и только она

В Go нет public и private. Видимость за пределами пакета решает одно правило из спецификации:

An identifier is exported if both: the first character of the identifier's name is a Unicode uppercase letter (Unicode character category Lu); and the identifier is declared in the package block or it is a field name or method name.

перевод

Идентификатор экспортируется, если выполнены оба условия: первый символ имени — заглавная буква Юникода (категория Lu); идентификатор объявлен в блоке пакета либо это имя поля или метода.

— Спецификация Go — Exported identifiers

Пакет shape для проверки — в нём неэкспортируемыми сделаны тип, поле, метод и функции, а экспортируемая New возвращает неэкспортируемый тип. Фрагмент cases/visibility/shape/shape.go, тела функций сокращены:

GO
type circle struct{ radius float64 }
type Box struct{ Width, height float64 }
 
func New(r float64) *circle       { return &circle{radius: r} }
func (c *circle) Area() float64   { ... }
func (c *circle) scale(k float64) { ... }
func area(r float64) float64      { ... }
func perimeter(r float64) float64 { ... }
func Perimeter(r float64) float64 { return perimeter(r) }
func Ωmega() string               { ... } // и ωmega, _Hidden

Из другого пакета — что можно:

Вывод bench/gopkg/internals.go, раздел 4:

4) ЭКСПОРТ — ПО ПЕРВОЙ БУКВЕ
────────────────────────────
go run ./visibility/ok
    c := shape.New(2)      type *shape.circle
    c.Area()               12.57
    var d = shape.New(1)   type *shape.circle
    b.Width, b.Height()    3 4
    {Width:3 height:4}
    Ωmega: exported (Ω is Lu)

И что нельзя — все девять обращений в одном файле (программа намеренно не собирается). Тексты — вывод gc go1.24.7: это наблюдение конкретной версии компилятора, а не формулировка спецификации; go1.25.1 к части сообщений добавляет подсказки вида (but have Ωmega).

Продолжение того же раздела:

go build ./visibility/bad
    visibility/bad/main.go:10:20: undefined: shape.area
    visibility/bad/main.go:11:20: name perimeter not exported by package shape
    visibility/bad/main.go:12:14: name circle not exported by package shape
    visibility/bad/main.go:15:16: p.radius undefined (cannot refer to unexported field radius)
    visibility/bad/main.go:16:4: p.scale undefined (cannot refer to unexported method scale)
    visibility/bad/main.go:18:16: b.height undefined (type shape.Box has no field or method height, but does have method Height)
    visibility/bad/main.go:19:26: cannot refer to unexported field height in struct literal of type shape.Box
    visibility/bad/main.go:20:20: undefined: shape.ωmega
    visibility/bad/main.go:21:20: undefined: shape._Hidden

Из этих двух выводов следуют три вещи, и каждую спрашивают отдельно.

Неэкспортируемый тип можно вернуть наружу. shape.New(2) вернул *shape.circle, и экспортируемый метод Area вызвался. Нельзя другое — назвать тип: name circle not exported.

Видимость решается для каждого имени отдельно. Box экспортирован, его поле height — нет: ни прочитать, ни задать в литерале. fmt при этом печатает его ({Width:3 height:4}): он читает поля через рефлексию, а правило экспорта ограничивает исходный код; чем ограничена сама рефлексия — в уроке о рефлексии.

«Заглавная» — значит категория Юникода Lu, а не «латинская большая». Ωmega экспортируется, ωmega — нет; _Hidden — нет, потому что первый символ — подчёркивание.

Строки 10 и 11 нарушают правило одинаково, а сообщения у них разные: undefined: shape.area и name perimeter not exported by package shape. Для ответа достаточно знать, что оба — ошибка компиляции; почему тексты расходятся, разобрано в «Глубже».

Механизм 3: internal/ — правило команды go, остальное — соглашения

Единственное запрещающее правило о раскладке каталогов, которое кто-то проверяет, — про каталог internal. Записано оно не в спецификации (слова internal в этом смысле там нет), а в документации команды go:

Code in or below a directory named "internal" is importable only by code in the directory tree rooted at the parent of "internal".

перевод

Код в каталоге с именем internal и ниже него может импортировать только код из дерева каталогов, корень которого — родитель internal.

— Команда go — go help gopath, Internal Directories

Раскладка в прогоне — app/internal/secret, рядом с ним app/cmd/server и app/api, а снаружи app — outsider и viaapi:

Вывод bench/gopkg/internals.go, раздел 6 (outsider и stdinternal намеренно не собираются; рабочий путь — viaapi):

6) internal/: ИМПОРТ ТОЛЬКО ИЗНУТРИ ДЕРЕВА РОДИТЕЛЯ
───────────────────────────────────────────────────
go run ./internalpkg/app/cmd/server   (внутри app/)
    app/cmd/server: s3cr3t
go run ./internalpkg/viaapi   (снаружи, через app/api)
    outsider via app/api: s3cr3t
go build ./internalpkg/outsider   (снаружи, напрямую)
    package deepengineering/bench/gopkg/cases/internalpkg/outsider
        internalpkg/outsider/main.go:6:2: use of internal package deepengineering/bench/gopkg/cases/internalpkg/app/internal/secret not allowed
go build ./internalpkg/stdinternal   (import "internal/cpu")
    package deepengineering/bench/gopkg/cases/internalpkg/stdinternal
        internalpkg/stdinternal/main.go:5:2: use of internal package internal/cpu not allowed
go build ./internalpkg/app/internal/secret   (сам пакет — из командной строки)
    собрано без ошибок

Родитель — это app, а не модуль. outsider лежит в том же модуле, но вне app — и получает отказ. internal можно положить на любой глубине, и каждый закрывает своё поддерево; internal/cpu стандартной библиотеки лежит в корне, и импортировать его может только она сама.

Закрыт импорт, а не данные. viaapi получил тот же s3cr3t через экспортируемый app/api: internal запрещает написать import, но не мешает пакетам внутри дерева отдавать что угодно наружу.

Проверяет команда go, а не компилятор. Сообщение печатает загрузчик пакетов команды go (cmd/go/internal/load/pkg.go, функция disallowInternal, строка 1472 в go1.24.7). Пакет, названный в командной строке, по комментарию в ней — не импорт, и правило к нему не применяется: поэтому go build ./internalpkg/app/internal/secret собрался.

cmd/ и pkg/ — соглашения, которых не проверяет никто

Особый смысл для команды go есть, например, у таких имён каталогов: internal, vendor, testdata и у всего, что начинается с . или _. cmd/ среди них нет:

Вывод bench/gopkg/internals.go, раздел 13 (importmain намеренно не собирается):

13) РАСКЛАДКА: cmd/ — СОГЛАШЕНИЕ, А НЕ ПРАВИЛО
──────────────────────────────────────────────
go build -o <tmp>/bin/ ./layout/cmd/greet ./layout/tools/anywhere
    бинарники: anywhere greet
go run ./layout/tools/anywhere
    package main in tools/anywhere: builds just the same
go build ./layout/importmain   (импортирует пакет main из cmd/greet)
    layout/importmain/main.go:6:2: import "deepengineering/bench/gopkg/cases/layout/cmd/greet" is a program, not an importable package

package main в tools/anywhere собрался так же, как в cmd/greet, а бинарники названы не по имени пакета main, а по последнему элементу пути импорта — обычно это имя каталога. Привычка класть команды в cmd/<имя>/ полезна ровно этим: имя каталога становится именем бинарника. pkg/ команда go тоже никак не толкует. Правило, которое действительно есть, — последняя строка: пакет main импортировать нельзя, поэтому всё, что нужно кому-то ещё, выносят из него в обычный пакет.

Механизм 4: порядок инициализации — по зависимостям, а не по тексту

Это главный вопрос лестницы, и у него три уровня: переменные внутри пакета, файлы и init(), пакеты между собой. Все три описаны в спецификации, и на каждом пересказ «сверху вниз» неверен.

Переменные. На каждом шаге берётся самая ранняя по объявлению переменная, которая уже готова:

Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables.

перевод

Внутри пакета переменные уровня пакета инициализируются по шагам, и на каждом шаге выбирается самая ранняя в порядке объявления переменная, у которой нет зависимостей от неинициализированных переменных.

— Спецификация Go — Package initialization

Фрагмент cases/initvars/main.go:

GO
var total = trace("total", price*qty)
var price = trace("price", 10)
var qty = trace("qty", 3)
 
func init() { fmt.Println("init()      total =", total) }

Вывод bench/gopkg/internals.go, раздел 7:

7) ПЕРЕМЕННЫЕ ПАКЕТА — ПО ЗАВИСИМОСТЯМ
──────────────────────────────────────
порядок в файле: total = price*qty, потом price, потом qty
go run ./initvars
    var price = 10
    var qty   = 3
    var total = 30
    init()      total = 30
    main()

total объявлена первой, но не готова: она ссылается на price и qty. Поэтому первой инициализирована price, затем qty, и только потом total. init() вызвана после всех переменных пакета и видит total = 30.

Файлы и init(). Пакет инициализируется целиком: сначала все переменные всех файлов, потом все init() — в порядке исходника, possibly in multiple files, as presented to the compiler (возможно, в нескольких файлах — так, как они поданы компилятору). «Как поданы компилятору» — не фигура речи. В initfiles четыре файла, в каждом переменная и init(), в main.go — две init(), а needsZ из a.go ссылается на fromZ из z.go:

Вывод bench/gopkg/internals.go, раздел 8:

8) НЕСКОЛЬКО ФАЙЛОВ, НЕСКОЛЬКО init()
─────────────────────────────────────
go list -f '{{.GoFiles}}' ./initfiles
    [a.go b.go main.go z.go]
go run ./initfiles
    var fromA (a.go)
    var fromB (b.go)
    var fromMain (main.go)
    var fromZ (z.go)
    var needsZ (a.go), uses fromZ (z.go)
    init() in a.go
    init() in b.go
    init() #1 in main.go
    init() #2 in main.go
    init() in z.go
    main()

Команда go подаёт файлы по имени — так советует спецификация ради воспроизводимости. Если перечислить их руками в обратном порядке (go run z.go main.go b.go a.go, тот же раздел прогона), переворачиваются и переменные, и init(). Две init() в одном файле законны и вызываются обе. А зависимость сильнее файла: needsZ объявлена в первом файле, а инициализирована последней из переменных — она ждала fromZ.

Пакеты. Пакет инициализируется после всех, кого импортирует, и один раз; среди готовых — первый по пути импорта:

Given the list of all packages, sorted by import path, in each step the first uninitialized package in the list for which all imported packages (if any) are already initialized is initialized.

перевод

Из списка всех пакетов, отсортированного по пути импорта, на каждом шаге инициализируется первый неинициализированный пакет, все импортируемые пакеты которого уже инициализированы.

— Спецификация Go — Program initialization

Вывод bench/gopkg/internals.go, раздел 9:

9) ПАКЕТЫ: ЗАВИСИМОСТИ РАНЬШЕ, КАЖДЫЙ ОДИН РАЗ
──────────────────────────────────────────────
main импортирует zeta, alpha, mid (в этом порядке); mid импортирует zeta
go run ./initdeps
    alpha: var Name
    alpha: init()
    zeta: var Name
    zeta: init()
    mid: var Name, zeta.Name = zeta
    mid: init()
    main: var name
    main: init()
    main() zeta alpha mid

alpha раньше zeta, хотя main импортирует zeta первой строкой: порядок импортов в файле не значит ничего, решает путь. mid по алфавиту раньше zeta, но ждёт её — mid её импортирует. zeta импортируют двое, а инициализирована она один раз. И каждый пакет — целиком: переменные, затем init(), и только потом следующий пакет.

Вывод bench/gopkg/internals.go, раздел 10 (программа намеренно не собирается):

10) init НЕЛЬЗЯ ВЫЗВАТЬ
───────────────────────
go build ./initcall   (main вызывает init())
    initcall/main.go:8:2: undefined: init

По спецификации у init нет ни параметров, ни результатов, а сам идентификатор init не объявляется — сослаться на функцию init нельзя нигде. Значит, ошибку она не вернёт, а повторить её, например из теста, не выйдет: всё, что может не получиться, делают в явной функции, которую вызывает main.

Механизм 5: пустой импорт ради init — и почему циклы запрещены

Импорт без обращений — ошибка. Но иногда пакет нужен только ради его init. Для этого спецификация оставила пустой идентификатор:

It is illegal for a package to import itself, directly or indirectly, or to directly import a package without referring to any of its exported identifiers. To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name.

перевод

Пакету запрещено импортировать самого себя, прямо или косвенно, и напрямую импортировать пакет, не обращаясь ни к одному из его экспортируемых идентификаторов. Чтобы импортировать пакет только ради побочных эффектов (инициализации), используйте пустой идентификатор как явное имя пакета.

— Спецификация Go — Import declarations

Канонический пример — форматы изображений. image.Decode умеет только те форматы, которые кто-то зарегистрировал, а регистрирует их init пакета формата (image/png/reader.go, строки 1052–1053: image.RegisterFormat("png", ...)):

Вывод bench/gopkg/internals.go, раздел 12 (unused намеренно не собирается; исправление — withpng):

12) ПУСТОЙ ИМПОРТ РАДИ init
───────────────────────────
go run ./blank/nopng   (импортирован только image)
    image.Decode: image: unknown format
go run ./blank/withpng   (плюс import _ "image/png")
    image.Decode: png 2 x 1
go build ./blank/unused   (import "image/png" без _ и без обращений)
    blank/unused/main.go:5:2: "image/png" imported and not used

Один и тот же PNG, один и тот же вызов; разница — init пакета image/png. Без подчёркивания такой импорт не собирается.

Драйверы database/sql устроены так же. В стандартной библиотеке драйверов нет, поэтому в прогоне вместо драйвера — заглушка fakedb, которая, как и настоящие, вызывает sql.Register в init. Продолжение раздела 12:

go run ./blank/nodriver   (только database/sql)
    sql.Drivers(): []
    sql.Open("fakedb"): sql: unknown driver "fakedb" (forgotten import?)
go run ./blank/withdriver   (плюс import _ ".../blank/fakedb")
    fakedb: init() -> sql.Register("fakedb", ...)
    sql.Drivers(): [fakedb]
    sql.Open("fakedb"): ok, db.Ping(): fakedb: not a real database

Подсказка (forgotten import?) вписана в текст ошибки прямо в исходнике (database/sql/sql.go, строка 868 в go1.24.7). Обратная сторона приёма тоже видна: init выполняется, даже если из пакета не вызвана ни одна функция, — импорт сам по себе имеет последствия.

Цикл импортов. Если orders импортирует users, а users — orders, ни один из них нельзя инициализировать «после всех, кого он импортирует». Спецификация запрещает такое, а команда go называет весь путь цикла:

Вывод bench/gopkg/internals.go, раздел 11 (программы намеренно не собираются):

11) ЦИКЛ ИМПОРТОВ
─────────────────
go build ./cycle/orders   (orders -> users -> orders)
    package deepengineering/bench/gopkg/cases/cycle/orders
        imports deepengineering/bench/gopkg/cases/cycle/users from orders.go
        imports deepengineering/bench/gopkg/cases/cycle/orders from users.go: import cycle not allowed
go build ./cycle/self   (пакет импортирует сам себя)
    package deepengineering/bench/gopkg/cases/cycle/self
        imports deepengineering/bench/gopkg/cases/cycle/self from self.go: import cycle not allowed

Запрет циклов и делает порядок «зависимости раньше» всегда выполнимым. Цена — в дизайне: пакеты, которым нужны типы друг друга, придётся развести.

Глубже: кто выстраивает пакеты, сколько их в сборке и откуда два сообщения

наблюдение замераbench/gopkg/internals.go, раздел 9, и bench/gopkg/practice.go, раздел 2: go1.24.7 linux/amd64, CGO_ENABLED=1, GOFLAGS пуст. GODEBUG=inittrace=1 печатает строку на каждый пакет с работой инициализации; времена из неё в записи прогона выброшены, оставлены имена и порядок. На go1.25.1 список пакетов с работой инициализации тот же, а пакетов в графе сборки больше: 64 и 61.

Порядок из Механизма 4 можно увидеть не только по строкам, которые печатает программа. Переменная окружения GODEBUG=inittrace=1 заставляет среду выполнения печатать строку на каждый пакет, у которого есть работа инициализации, — в том порядке, в каком они инициализируются. Конец вывода раздела 9:

GODEBUG=inittrace=1 ./initdeps   (пакеты с работой инициализации, по порядку)
    стандартная библиотека, 14: internal/bytealg runtime math errors iter sync internal/godebug syscall time internal/poll io/fs os unicode reflect
    наши, 4: initdeps/alpha initdeps/zeta initdeps/mid main
    всего пакетов в сборке (go list -deps): 59

Из 59 пакетов графа сборки (в их числе псевдопакет unsafe и сам main) строку получили 18. У остальных нет работы инициализации, и компилятор даже не создаёт для них запись (cmd/compile/internal/pkginit/init.go, строки 120–122); по документации пакета runtime, ничего не печатается для пакетов without both user defined and compiler generated init work (без работы инициализации — ни пользовательской, ни сгенерированной компилятором).

деталь реализации · Go 1.24cmd/link/internal/ld/inittask.go, строки 15–39 (go1.24.7): компилятор записывает для каждого пакета, которому есть что инициализировать, запись инициализации со ссылками на записи импортируемых пакетов, а компоновщик вычисляет порядок, который соблюдает все зависимости и в остальном лексикографический. Среда выполнения исполняет готовый список.

Вычисляет этот порядок компоновщик. Каждый пакет, которому есть что инициализировать, при компиляции получает запись инициализации со ссылками на записи импортируемых пакетов, а компоновщик упорядочивает все записи так, чтобы соблюсти зависимости, а в остальном — лексикографически. Это и есть правило спецификации «по пути импорта, но зависимости раньше»; среда выполнения при старте лишь проходит готовый список.

Правило «по пути импорта» появилось не сразу — это в «Истории версий» ниже. Отсюда практический вывод: полагаться можно только на порядок, выраженный импортом. Если пакет B должен быть инициализирован раньше A, A должен импортировать B; порядок «по алфавиту» завязывает программу на имена каталогов. И оговорка спецификации: зависимости между переменными она видит только по ссылкам в исходнике пакета. Если зависимость скрыта — например, за вызовом метода интерфейса, — порядок между такими переменными не определён.

Факультативный эксперимент: сколько пакетов в графе сборки

Это не знание о языке, которое стоит запоминать, а наблюдение одной конфигурации: число зависит от версии Go, GOOS/GOARCH и настроек сборки. Повторить его можно командой go list -deps . в каталоге программы с одним import "fmt".

Вывод bench/gopkg/practice.go, раздел 2:

2) ЭКСПЕРИМЕНТ: СКОЛЬКО ПАКЕТОВ В ГРАФЕ СБОРКИ
──────────────────────────────────────────────
программа: package main, import "fmt", fmt.Println("hello")
  конфигурация: go1.24.7, GOOS/GOARCH/CGO_ENABLED = linux/amd64/1, GOFLAGS пуст
  пакетов в графе сборки (go list -deps, вместе с main и unsafe): 56
  из них с работой инициализации (GODEBUG=inittrace=1): 14
  internal/bytealg runtime math errors iter sync internal/godebug syscall time internal/poll io/fs os unicode reflect

На go1.25.1 в той же конфигурации пакетов 61, а с работой инициализации — те же 14. Устойчиво здесь одно: все пакеты графа проходят инициализацию до main, а строку inittrace получают только те, кому есть что делать.

Одно нарушение — два сообщения: данные экспорта и встраивание

Вернёмся к строкам 10 и 11 из Механизма 2: обе обращаются к неэкспортируемой функции, а сообщения разные: undefined: shape.area и name perimeter not exported by package shape. Если запретить встраивание — флагом для всей сборки или директивой //go:noinline на Perimeter, — разница пропадает.

Вывод bench/gopkg/internals.go, раздел 5 (программы намеренно не собираются):

5) ОДНО НАРУШЕНИЕ — ДВА СООБЩЕНИЯ
─────────────────────────────────
go build -gcflags=all=-l ./visibility/bad   (встраивание выключено)
    visibility/bad/main.go:10:20: undefined: shape.area
    visibility/bad/main.go:11:20: undefined: shape.perimeter
go build ./visibility/noinline/bad   (та же пара, Perimeter помечена //go:noinline)
    visibility/noinline/bad/main.go:10:20: undefined: shape.perimeter
деталь реализации · Go 1.24cmd/compile/internal/types2/call.go, строки 720–728 (go1.24.7): имя ищется в области видимости импортированного пакета; не найдено — «undefined», найдено, но не экспортировано — сообщение not exported by package. Что неэкспортируемое имя попадает в данные экспорта из-за встраиваемой Perimeter, показывает прогон: и с -gcflags=all=-l, и с //go:noinline на одной Perimeter сообщение становится «undefined». go1.25.1 к «undefined» добавляет подсказку (but have Perimeter).

Импортированный пакет компилятор видит не по исходнику, а по данным экспорта — тому, что пакет сообщил о себе. perimeter туда попала: её вызывает короткая Perimeter, которую можно встроить в чужой код. area снаружи не нужна никому, и компилятор её просто не видит. Знать это нужно для одного: undefined: pkg.name не значит, что такого имени в пакете нет — оно может быть, но с маленькой буквы.

Как отвечать на собеседовании

Короткий ответ: пакет — каталог файлов с одним именем в строке package. Снаружи видны только имена с заглавной буквы. Инициализация идёт так: сначала пакеты, которые импортированы, — каждый один раз; внутри пакета — переменные по зависимостям, затем все init() в порядке файлов; и только потом main.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «в каком порядке init» покажите переменную, объявленную первой. «var total = price*qty объявлена до price и qty, а в прогоне инициализирована третьей. Правило — самая ранняя по объявлению переменная, у которой зависимости готовы. init() — после всех переменных пакета».

На «а между пакетами» — зависимости, потом путь импорта. «Пакет инициализируется после всех, кого импортирует, и один раз. Независимые — по пути импорта: в прогоне alpha раньше zeta, хотя main импортирует zeta первым. Порядок импортов в файле значения не имеет».

На «зачем internal» — и кто его проверяет. «Импорт пакета из internal/ разрешён только коду из дерева родителя internal. Это правило команды go — use of internal package ... not allowed, — а не языка. Закрыт импорт, а не данные: экспортируемый пакет рядом может их отдать».

На «как разложить проект» — разделите правило и привычку. «cmd/<имя>/ для команд — соглашение: бинарник называется по последнему элементу пути, а package main собирается где угодно. Правила два: пакет main нельзя импортировать, и internal/ закрывает поддерево».

На «зачем _ в импорте» — init и регистрация. «import _ "image/png" выполняет init пакета, который регистрирует декодер; без него image.Decode отвечает image: unknown format. Драйверы database/sql так же регистрируются в init».

Дальше спросят

Спросят дальше

Можно ли вернуть из экспортируемой функции значение неэкспортируемого типа?

Короткий ответ

Можно, и компилятор не возражает: shape.New(2) в прогоне вернул *shape.circle, и вызывающий вызвал у него экспортируемый метод Area. Нельзя другое — назвать этот тип снаружи (строка из раздела 4):

    visibility/bad/main.go:12:14: name circle not exported by package shape

Значит, снаружи нельзя написать этот тип: в поле структуры, в параметре функции, в var x shape.circle. Получить значение можно только с выводом типа — := или var d = shape.New(1). Для API это неудобно: пользователь не может объявить поле такого типа в своей структуре. Поэтому наружу обычно отдают экспортируемый тип с неэкспортируемыми полями или интерфейс.

Спросят дальше

Как в одном каталоге оказываются пакеты x и x_test?

Короткий ответ

Это единственное исключение из правила «одно имя на каталог». Файл, имя которого кончается на _test.go, может объявить package x_test — и станет внешним тестом: отдельным пакетом, который импортирует x как обычный пользователь и видит только экспортируемое. go list показывает такие файлы отдельно (строки из раздела 2):

    TestGoFiles=[listing_test.go]
    XTestGoFiles=[api_test.go]

Исключение проверяется по имени файла (go/build/build.go, строки 948–953 в go1.24.7): в файле без суффикса _test.go то же package util_test даёт обычную ошибку found packages util_test (helper.go) and util (util.go). Внешний тест видит пакет так же, как его пользователь, — документация пакета testing называет это "black box" testing (тестированием «чёрного ящика») и уточняет, что такому тесту доступны only its exported identifiers (только экспортируемые идентификаторы).

Спросят дальше

Два пакета нужны друг другу. Что делать с циклом импортов?

Короткий ответ

Сначала понять, что именно нужно. Если это типы — orders нужен users.User, а users — *orders.Order, — их выносят в третий пакет, который импортируют оба, — цикл исчезает. Если же одному пакету нужно поведение другого, его заменяют интерфейсом, объявленным на стороне того, кто вызывает: пакет зависит от интерфейса, который объявил сам, а не от пакета, который его реализует.

Склеить оба пакета в один — тоже выход: цикл часто значит, что граница между пакетами проведена не там. Начинают с сообщения import cycle not allowed — оно перечисляет весь путь цикла с файлами, где стоят импорты.

Частые заблуждения

Утверждение

init() — первое, что выполняется в пакете

На самом деле

Сначала инициализируются все переменные пакета, во всех файлах, и только потом вызываются init(). В прогоне init() увидел total = 30 — значение, вычисленное до него.

Утверждение

Переменные пакета инициализируются сверху вниз

На самом деле

По зависимостям: на каждом шаге — самая ранняя по объявлению из готовых. var total = price*qty, объявленная первой, инициализирована третьей.

Утверждение

Пакеты инициализируются в порядке импортов в файле

На самом деле

Зависимости — раньше, независимые — по пути импорта. main импортирует zeta первым, а alpha в прогоне инициализирован раньше. Порядок строк import не значит ничего.

Утверждение

Имя пакета — это последний элемент пути импорта

На самом деле

Это соглашение. Имя — то, что стоит в строке package: каталог naming/strutil содержит package textkit, и обращение strutil.Shout даёт undefined: strutil.

Утверждение

internal — правило компилятора или языка

На самом деле

В спецификации его нет. Импорт запрещает команда go — функция disallowInternal в её загрузчике пакетов — с сообщением use of internal package ... not allowed. Пакет, названный в командной строке, она не останавливает.

Утверждение

Проект на Go обязан иметь cmd/ и pkg/

На самом деле

Это соглашения. package main в tools/anywhere собирается так же, как в cmd/greet, а бинарник называется по последнему элементу пути импорта. Особый смысл для команды go есть, например, у internal, vendor, testdata и имён на . и _.

Утверждение

undefined: pkg.name значит, что такого имени в пакете нет

На самом деле

Не обязательно: shape.area в пакете есть, но с маленькой буквы, а компилятор (gc go1.24.7) сообщил undefined. Сообщение not exported by package появляется, только если имя попало в данные экспорта, — как perimeter, которую вызывает встраиваемая Perimeter.

Утверждение

init можно вызвать вручную — например, в тесте

На самом деле

Нельзя: undefined: init. Идентификатор init по спецификации не объявляется, и сослаться на функцию init нельзя нигде в программе.

История версий

ВерсияИзменениеЧто это значит для кода
go1.13 и раньшеПо комментарию в регрессионном тесте test/fixedbugs/issue31636.dir/main.go, независимые пакеты инициализировались в порядке импортов в исходнике; там же сказано, что с 1.14 реализация переходит к варианту лексикографического порядка. Тест импортирует c, b, a и сейчас ждёт вывода a b c.порядок независимых пакетов зависел от строк import
спецификация go1.17Сказано только, что импортированные пакеты инициализируются раньше импортирующего и один раз. Правила «по пути импорта» нет (doc/go1.17_spec.html в поставке go1.24.7).порядок независимых пакетов спецификацией не определён
спецификация go1.24.7 (локальная копия)Правило есть: из списка пакетов, отсортированного по пути импорта, на каждом шаге — первый, чьи импорты уже инициализированы. В спецификации go1.17 его ещё нет; в какой версии между ними оно появилось, локальные источники не говорят. Прогон на go1.24.7 и go1.25.1: alpha, zeta, mid, main.порядок определён, но полагаться стоит только на импорт
go1.25.1К сообщению undefined компилятор добавляет подсказку: undefined: shape.perimeter (but have Perimeter) (когда встраивание запрещено: -gcflags=all=-l или //go:noinline; при обычной сборке остаётся name perimeter not exported), undefined: shape.ωmega (but have Ωmega). В графе сборки программы с одним fmt на пять пакетов больше, чем на go1.24.7.только текст сообщений и состав сборки
Go 1.26 и 1.27Урок их не измерял: прогоны — go1.24.7 и go1.25.1 на linux/amd64, сверено 14.10.2026. Нормативный источник порядка инициализации — текущая спецификация, раздел Program initialization; тексты диагностик и состав графа сборки в новых версиях могут отличаться.правила — по спецификации; числа и тексты сообщений — мерить у себя

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Программа из двух пакетов: main импортирует dep. В main переменная total объявлена раньше price и qty, от которых зависит, и функций init две. Что напечатает программа?
// dep/dep.go
package dep

import "fmt"

var Ready = mark("dep: var Ready")

func mark(s string) bool {
fmt.Println(s)
return true
}

func init() { fmt.Println("dep: init") }

// main.go
package main

import (
"fmt"

"example/dep"
)

var total = trace("total", price*qty)
var price = trace("price", 10)
var qty = trace("qty", 3)

func trace(name string, v int) int {
fmt.Println("main: var", name)
return v
}

func init() { fmt.Println("main: init 1, total =", total) }

func init() { fmt.Println("main: init 2") }

func main() { fmt.Println("main: main, dep.Ready =", dep.Ready) }

Практика · оцените

Пакеты a, b и c импортируют общий пакет reg; main импортирует a, b и c. В каждом из пяти пакетов ровно одна функция init, которая печатает одну строку. Сколько строк напечатают функции init до начала main?
строк

Проверка знаний

Вопрос 1 из 5

В пакете shape объявлена функция area с маленькой буквы. Что будет, если вызвать shape.area(1) из другого пакета?

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Спецификация Go — Exported identifiersОфициальная документация. Правило экспорта целиком: «An identifier is exported if both: the first character of the identifier's name is a Unicode uppercase letter (Unicode character category Lu); and the identifier is declared in the package block or it is a field name or method name» (Идентификатор экспортируется, если выполнены оба условия: первый символ имени — заглавная буква Юникода (категория Lu); идентификатор объявлен в блоке пакета либо это имя поля или метода). И: «All other identifiers are not exported» (Все остальные идентификаторы не экспортируются). go_spec.html go1.24.7, строки 2396–2411.https://go.dev/ref/spec#Exported_identifiers
  2. Спецификация Go — Package clause и Import declarationsОфициальная документация. Что такое пакет: «A set of files sharing the same PackageName form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory» (Набор файлов с одним и тем же PackageName образует реализацию пакета. Реализация может требовать, чтобы все исходные файлы пакета лежали в одном каталоге). Что запрещено при импорте и зачем пустой идентификатор: «It is illegal for a package to import itself, directly or indirectly, or to directly import a package without referring to any of its exported identifiers. To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name» (Пакету запрещено импортировать самого себя, прямо или косвенно, и напрямую импортировать пакет, не обращаясь ни к одному из его экспортируемых идентификаторов. Чтобы импортировать пакет только ради побочных эффектов (инициализации), используйте пустой идентификатор как явное имя пакета). go_spec.html go1.24.7, строки 8013–8014 и 8084–8088.https://go.dev/ref/spec#Import_declarations
  3. Спецификация Go — Package initialization и Program initializationОфициальная документация. Переменные: «Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables» (Внутри пакета переменные уровня пакета инициализируются по шагам, и на каждом шаге выбирается самая ранняя в порядке объявления переменная, у которой нет зависимостей от неинициализированных переменных). init: «The entire package is initialized by assigning initial values to all its package-level variables followed by calling all init functions in the order they appear in the source, possibly in multiple files, as presented to the compiler» (Пакет инициализируется целиком: сначала всем переменным уровня пакета присваиваются начальные значения, затем вызываются все функции init в том порядке, в каком они встречаются в исходнике, возможно, в нескольких файлах — так, как файлы поданы компилятору). Пакеты: «If multiple packages import a package, the imported package will be initialized only once» (Если пакет импортируют несколько пакетов, он будет инициализирован только один раз) и «Given the list of all packages, sorted by import path, in each step the first uninitialized package in the list for which all imported packages (if any) are already initialized is initialized» (Из списка всех пакетов, отсортированного по пути импорта, на каждом шаге инициализируется первый неинициализированный пакет, все импортируемые пакеты которого уже инициализированы). go_spec.html go1.24.7, строки 8194–8385.https://go.dev/ref/spec#Package_initialization
  4. Команда go — Internal Directories (go help gopath)Официальная документация. Правило internal записано в документации команды go, а не в спецификации: «Code in or below a directory named "internal" is importable only by code in the directory tree rooted at the parent of "internal"» (Код в каталоге с именем internal и ниже него может импортировать только код из дерева каталогов, корень которого — родитель internal). Текст — cmd/go/internal/help/helpdoc.go, строка 391 (go1.24.7); проверка — cmd/go/internal/load/pkg.go, функция disallowInternal, строка 1472.https://pkg.go.dev/cmd/go#hdr-Internal_Directories
  5. Команда go — Package lists and patterns (go help packages)Официальная документация. Имя пакета не обязано быть уникальным, а пакет main не импортируется: «Packages in a program need not have unique package names, but there are two reserved package names with special meaning. The name main indicates a command, not a library. Commands are built into binaries and cannot be imported» (Имена пакетов в программе не обязаны быть уникальными, но есть два зарезервированных имени с особым смыслом. Имя main обозначает команду, а не библиотеку. Команды собираются в бинарники и не могут быть импортированы). И что команда go пропускает: «Directory and file names that begin with "." or "_" are ignored by the go tool, as are directories named "testdata"» (Каталоги и файлы, чьи имена начинаются с . или _, команда go пропускает, как и каталоги с именем testdata).https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns
  6. Пакет imageОфициальная документация. Зачем пустой импорт в стандартной библиотеке: «Decoding any particular image format requires the prior registration of a decoder function. Registration is typically automatic as a side effect of initializing that format's package so that, to decode a PNG image, it suffices to have import _ "image/png" in a program's main package» (Чтобы декодировать изображение в каком-либо формате, нужно заранее зарегистрировать функцию-декодер. Обычно регистрация происходит автоматически как побочный эффект инициализации пакета этого формата, так что для декодирования PNG достаточно написать import _ "image/png" в пакете main программы). Регистрация — image/png/reader.go, строки 1052–1053.https://pkg.go.dev/image