Deep Engineering

ЗАМЕР

bench/gopkg/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/packages
Как запустить
go run bench/gopkg/internals.go
/usr/local/go1.25.1/bin/go run bench/gopkg/internals.go
go run bench/gopkg/practice.go
/usr/local/go1.25.1/bin/go run bench/gopkg/practice.go

Запись прогона

Замеры для урока «Пакеты в Go»

Файл Что делает
internals.go четырнадцать наблюдений, каждое — сборка, запуск или go list подопытной программы из cases/: одно имя пакета на каталог, что входит в пакет, путь импорта против имени пакета, экспорт по первой букве и два разных сообщения компилятора на одно нарушение, internal/, порядок инициализации переменных, файлов и init(), порядок пакетов и GODEBUG=inittrace=1, init нельзя вызвать, цикл импортов, пустой импорт ради init (image/png, драйвер database/sql), cmd/ как соглашение и пакет main, который нельзя импортировать; полный пример из двух пакетов
practice.go источник ответов двух практических задач урока: вывод программы из двух пакетов с init и переменными не по порядку (раздел 1); сколько строк init до main, когда три пакета импортируют общий reg (раздел 3). Раздел 2 — факультативный эксперимент: сколько пакетов в графе сборки программы с одним fmt, с конфигурацией сборки
cases/ отдельный модуль deepengineering/bench/gopkg/cases; часть программ намеренно не собирается
cases/twopkg, cases/xtestname два имени пакета в одном каталоге; package util_test в файле без суффикса _test.go
cases/listing пакет с файлами под две ОС, //go:build ignore, внутренним и внешним тестом, _draft.go и testdata/
cases/naming каталог strutil с пакетом textkit; импорт с псевдонимом; обращение по имени каталога
cases/visibility shape с неэкспортируемыми типом, полем, методом и функциями; ok пользуется разрешённым, bad — запрещённым; noinline — та же пара perimeter/Perimeter с //go:noinline
cases/internalpkg app/internal/secret и кто может его импортировать; internal/cpu из стандартной библиотеки
cases/initvars, cases/initfiles, cases/initdeps, cases/initcall порядок инициализации: переменные, файлы и init(), пакеты; вызов init()
cases/cycle два пакета, импортирующих друг друга, и пакет, импортирующий сам себя
cases/blank image.Decode без image/png и с ним; неиспользуемый импорт; драйвер-заглушка fakedb, регистрирующийся в init
cases/example полный пример урока — отдельный модуль example.com/hello (go.mod, main.go, greet/greet.go); go run . в этом каталоге, раздел 14
cases/layout package main в cmd/greet и в tools/anywhere; попытка импортировать пакет main
go run bench/gopkg/internals.go
/usr/local/go1.25.1/bin/go run bench/gopkg/internals.go
go run bench/gopkg/practice.go
/usr/local/go1.25.1/bin/go run bench/gopkg/practice.go

Записи прогонов — в runs/: internals.txt и practice.txt — go1.24.7 linux/amd64, *-go1.25.txt — go1.25.1. Абсолютный путь к cases/ заменён плейсхолдером <cases>, временный каталог — <tmp>. Времена из GODEBUG=inittrace=1 не печатаются: остаются только имена пакетов и их порядок.

Что получилось

Пакет — это каталог, и имя у него одно. Два файла с package alpha и package beta в одном каталоге: found packages alpha (alpha.go) and beta (beta.go). Исключение только одно — package util_test в файле _test.go; в обычном файле тот же util_test даёт ту же ошибку.

Путь импорта и имя пакета — разные вещи. Каталог naming/strutil содержит package textkit; импортируют его по пути, обращаются — textkit.Shout. Обращение strutil.Shout даёт две ошибки: imported as textkit and not used и undefined: strutil.

Экспорт решает первая буква — и только категория Lu. Из другого пакета нельзя обратиться к area, circle, полю radius, методу scale, полю height (в том числе в составном литерале), к ωmega и _Hidden; Ωmega экспортируется. Но shape.New(2) возвращает значение неэкспортируемого типа *shape.circle, и его метод Area вызывается.

Одно нарушение — два сообщения. shape.area даёт undefined: shape.area, shape.perimeter — name perimeter not exported by package shape. Разница — в том, попало ли имя в данные экспорта: perimeter вызывается из встраиваемой Perimeter. С -gcflags=all=-l оба — undefined; то же даёт //go:noinline на одной Perimeter (cases/visibility/noinline). go1.25.1 добавляет подсказку (but have Perimeter).

internal/ проверяет команда go. Импорт app/internal/secret из app/cmd/server собирается, из outsider — use of internal package … not allowed; то же для internal/cpu. Через экспортируемый app/api — работает. Сам пакет, названный в командной строке, собирается.

Переменные — по зависимостям, init — по файлам. total = price*qty, объявленная первой, инициализируется последней. Переменные нескольких файлов идут в порядке a.go b.go main.go z.go — так их подаёт компилятору команда go, и тот же список показывает go list, — кроме needsZ из a.go, которая ждёт fromZ из z.go. go run z.go main.go b.go a.go переворачивает порядок и переменных, и init. Две init в одном файле вызываются обе, по порядку.

Пакеты — зависимости раньше, остальное по пути импорта, каждый один раз. main импортирует zeta, alpha, mid; mid импортирует zeta. Порядок: alpha, zeta, mid, main; zeta инициализирован один раз. Из 59 пакетов сборки работу инициализации делают 14 пакетов стандартной библиотеки и 4 наших.

init нельзя вызвать, цикл импортов запрещён. undefined: init; import cycle not allowed — и для двух пакетов, и для пакета, импортирующего сам себя.

Пустой импорт — ради init. Без image/png image.Decode отвечает image: unknown format, с import _ "image/png" — png 2 x 1. Обычный импорт без обращений не собирается. Драйвер-заглушка регистрируется в init: sql.Drivers() — [] без него и [fakedb] с ним; без импорта sql.Open отвечает unknown driver "fakedb" (forgotten import?).

cmd/ — соглашение. package main в tools/anywhere собирается так же, как в cmd/greet; бинарники называются по последнему элементу пути импорта — здесь это имя каталога. Пакет main импортировать нельзя: is a program, not an importable package.

Программа с одним fmt — 56 пакетов в графе go list -deps, считая main и псевдопакет unsafe (go1.25.1 — 61); работу инициализации делают 14 из них.

Общая зависимость инициализируется один раз (practice.go, раздел 3). a, b и c импортируют reg, main импортирует их: строк init до main — 5, а не 7; reg — первым, main — последним.

Полный пример (cases/example, раздел 14): go run . печатает hello, gopher (init ran: true).

Прогоны сверены 14.10.2026 на go1.24.7 и go1.25.1, linux/amd64. Тексты диагностик и число пакетов в графе сборки — наблюдения этих версий, а не правила языка.

Скрипт

333 строк
//go:build ignore

// Пакеты в Go: что из правил видимости, инициализации и раскладки проверяет
// компилятор, что — команда go, а что остаётся соглашением.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопросы о пакетах на собеседовании звучат как
// справочные — «как сделать имя приватным», «в каком порядке вызываются
// init», «зачем internal». Отличает знающего не формулировка правила, а
// точность: какое сообщение напишет компилятор, кто именно запрещает импорт
// internal, почему переменная, объявленная первой, инициализируется
// последней. Здесь каждое такое поведение получено сборкой или запуском
// подопытной программы из cases/, а не пересказано.
//
// ЧЕТЫРНАДЦАТЬ СЮЖЕТОВ:
//
//  1. Пакет — это каталог: два имени пакета в одном каталоге — ошибка.
//  2. Что входит в пакет: go list по каталогу с тестами, файлами под другую
//     ОС, //go:build ignore, _draft.go и testdata.
//  3. Путь импорта и имя пакета — разные вещи.
//  4. Экспорт по первой букве: что можно и чего нельзя из другого пакета.
//  5. Одно нарушение — два разных сообщения компилятора (и почему).
//  6. internal/: кому импорт разрешён, кому нет.
//  7. Переменные пакета инициализируются по зависимостям, а не по порядку в
//     файле.
//  8. Несколько файлов и несколько init(): порядок — как файлы поданы
//     компилятору.
//  9. Пакеты: зависимости раньше, каждый один раз, остальное — по пути
//     импорта; и что показывает GODEBUG=inittrace=1.
//  10. init нельзя вызвать.
//  11. Цикл импортов.
//  12. Пустой импорт ради init: image/png и драйвер database/sql.
//  13. Раскладка: cmd/ — соглашение; пакет main нельзя импортировать.
//  14. Полный пример: модуль cases/example из двух пакетов, go run .
//
// Подопытные программы лежат в cases/ — отдельный модуль со своим go.mod.
// Часть из них намеренно не собирается. Абсолютный путь к cases/ и
// временные каталоги в выводе заменены плейсхолдерами <cases> и <tmp>.
//
// ЗАПУСК:
//
//	go run bench/gopkg/internals.go
//	/usr/local/go1.25.1/bin/go run bench/gopkg/internals.go
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
)

var (
	cases string // bench/gopkg/cases
	work  string // временный каталог для бинарников
	goBin string // go той же версии, что запустила программу
)

type result struct {
	out  string // stdout и stderr вместе
	err  string // только stderr (когда собран отдельно)
	code int
}

// run запускает команду в каталоге cases с добавочными переменными окружения.
func run(env []string, name string, args ...string) result {
	cmd := exec.Command(name, args...)
	cmd.Dir = cases
	cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
	out, err := cmd.CombinedOutput()
	code := 0
	if ee, ok := err.(*exec.ExitError); ok {
		code = ee.ExitCode()
	} else if err != nil {
		panic(err)
	}
	return result{out: string(out), code: code}
}

// runSplit — то же, но stdout и stderr раздельно.
func runSplit(env []string, name string, args ...string) result {
	cmd := exec.Command(name, args...)
	cmd.Dir = cases
	cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
	var so, se strings.Builder
	cmd.Stdout, cmd.Stderr = &so, &se
	err := cmd.Run()
	code := 0
	if ee, ok := err.(*exec.ExitError); ok {
		code = ee.ExitCode()
	} else if err != nil {
		panic(err)
	}
	return result{out: so.String(), err: se.String(), code: code}
}

func goCmd(args ...string) result { return run(nil, goBin, args...) }

func clean(s string) string {
	s = strings.ReplaceAll(s, cases, "<cases>")
	s = strings.ReplaceAll(s, work, "<tmp>")
	return s
}

// lines печатает строки вывода с отступом, пропуская строки «# пакет».
func lines(out string, indent string) {
	for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
		if strings.HasPrefix(l, "# ") || strings.TrimSpace(l) == "" {
			continue
		}
		l = strings.ReplaceAll(strings.TrimRight(clean(l), " \t"), "\t", "    ")
		fmt.Println(indent + l)
	}
}

func show(title string) {
	fmt.Println()
	fmt.Println(title)
	fmt.Println(strings.Repeat("─", len([]rune(title))))
}

// build собирает пакет и печатает либо «собрано», либо ошибки компилятора.
func build(label string, args ...string) {
	r := goCmd(append([]string{"build", "-o", os.DevNull}, args...)...)
	fmt.Println(label)
	if r.code == 0 {
		fmt.Println("    собрано без ошибок")
		return
	}
	lines(r.out, "    ")
}

// gorun запускает пакет и печатает его вывод.
func gorun(label string, pkg string) {
	r := goCmd("run", pkg)
	fmt.Println(label)
	lines(r.out, "    ")
}

const mod = "deepengineering/bench/gopkg/cases/"

func main() {
	_, file, _, _ := runtime.Caller(0)
	cases = filepath.Join(filepath.Dir(file), "cases")
	goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
	if _, err := os.Stat(goBin); err != nil {
		goBin = "go"
	}
	var err error
	work, err = os.MkdirTemp("", "gopkg")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(work)

	fmt.Printf("%s %s/%s | пакеты: видимость, init, internal, раскладка\n",
		runtime.Version(), runtime.GOOS, runtime.GOARCH)

	// ------------------------------------------------------------------ 1
	show("1) ПАКЕТ — ЭТО КАТАЛОГ: ОДНО ИМЯ НА КАТАЛОГ")
	build("go build ./twopkg   (alpha.go: package alpha, beta.go: package beta)", "./twopkg")
	build("go build ./xtestname   (util.go: package util, helper.go: package util_test)", "./xtestname")

	// ------------------------------------------------------------------ 2
	show("2) ЧТО ВХОДИТ В ПАКЕТ: go list")
	fmt.Println("файлы в cases/listing:")
	ents, _ := os.ReadDir(filepath.Join(cases, "listing"))
	var names []string
	for _, e := range ents {
		n := e.Name()
		if e.IsDir() {
			n += "/"
		}
		names = append(names, n)
	}
	fmt.Println("    " + strings.Join(names, " "))
	for _, goos := range []string{"linux", "windows"} {
		r := run([]string{"GOOS=" + goos}, goBin, "list", "-f",
			"Name={{.Name}}\nGoFiles={{.GoFiles}}\nIgnoredGoFiles={{.IgnoredGoFiles}}\nTestGoFiles={{.TestGoFiles}}\nXTestGoFiles={{.XTestGoFiles}}",
			"./listing")
		fmt.Printf("GOOS=%s go list -f '…' ./listing\n", goos)
		lines(r.out, "    ")
	}
	r := goCmd("vet", "./listing")
	fmt.Printf("go vet ./listing: код выхода %d, вывод %q\n", r.code, strings.TrimSpace(r.out))

	// ------------------------------------------------------------------ 3
	show("3) ПУТЬ ИМПОРТА И ИМЯ ПАКЕТА — РАЗНЫЕ ВЕЩИ")
	r = goCmd("list", "-f", "{{.ImportPath}}  ->  package {{.Name}}", "./naming/strutil")
	fmt.Println("go list -f '{{.ImportPath}} -> package {{.Name}}' ./naming/strutil")
	lines(r.out, "    ")
	gorun("go run ./naming   (import \".../naming/strutil\"; textkit.Shout(\"hello\"))", "./naming")
	gorun("go run ./naming/alias   (tk \".../naming/strutil\"; tk.Shout(\"alias\"))", "./naming/alias")
	build("go build ./naming/wrongname   (strutil.Shout — по имени каталога)", "./naming/wrongname")

	// ------------------------------------------------------------------ 4
	show("4) ЭКСПОРТ — ПО ПЕРВОЙ БУКВЕ")
	gorun("go run ./visibility/ok", "./visibility/ok")
	build("go build ./visibility/bad", "./visibility/bad")

	// ------------------------------------------------------------------ 5
	show("5) ОДНО НАРУШЕНИЕ — ДВА СООБЩЕНИЯ")
	for _, flags := range [][]string{nil, {"-gcflags=all=-l"}} {
		args := append(append([]string{}, flags...), "./visibility/bad")
		r := goCmd(append([]string{"build", "-o", os.DevNull}, args...)...)
		label := "go build ./visibility/bad"
		if flags != nil {
			label = "go build -gcflags=all=-l ./visibility/bad   (встраивание выключено)"
		}
		fmt.Println(label)
		for _, l := range strings.Split(r.out, "\n") {
			if strings.Contains(l, "area") || strings.Contains(l, "perimeter") {
				fmt.Println("    " + l)
			}
		}
	}
	build("go build ./visibility/noinline/bad   (та же пара, Perimeter помечена //go:noinline)", "./visibility/noinline/bad")

	// ------------------------------------------------------------------ 6
	show("6) internal/: ИМПОРТ ТОЛЬКО ИЗНУТРИ ДЕРЕВА РОДИТЕЛЯ")
	gorun("go run ./internalpkg/app/cmd/server   (внутри app/)", "./internalpkg/app/cmd/server")
	gorun("go run ./internalpkg/viaapi   (снаружи, через app/api)", "./internalpkg/viaapi")
	build("go build ./internalpkg/outsider   (снаружи, напрямую)", "./internalpkg/outsider")
	build("go build ./internalpkg/stdinternal   (import \"internal/cpu\")", "./internalpkg/stdinternal")
	build("go build ./internalpkg/app/internal/secret   (сам пакет — из командной строки)", "./internalpkg/app/internal/secret")

	// ------------------------------------------------------------------ 7
	show("7) ПЕРЕМЕННЫЕ ПАКЕТА — ПО ЗАВИСИМОСТЯМ")
	fmt.Println("порядок в файле: total = price*qty, потом price, потом qty")
	gorun("go run ./initvars", "./initvars")

	// ------------------------------------------------------------------ 8
	show("8) НЕСКОЛЬКО ФАЙЛОВ, НЕСКОЛЬКО init()")
	r = goCmd("list", "-f", "{{.GoFiles}}", "./initfiles")
	fmt.Println("go list -f '{{.GoFiles}}' ./initfiles")
	lines(r.out, "    ")
	gorun("go run ./initfiles", "./initfiles")
	{
		cmd := exec.Command(goBin, "run", "z.go", "main.go", "b.go", "a.go")
		cmd.Dir = filepath.Join(cases, "initfiles")
		cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS=")
		out, _ := cmd.CombinedOutput()
		fmt.Println("go run z.go main.go b.go a.go   (файлы — в обратном порядке)")
		lines(string(out), "    ")
	}

	// ------------------------------------------------------------------ 9
	show("9) ПАКЕТЫ: ЗАВИСИМОСТИ РАНЬШЕ, КАЖДЫЙ ОДИН РАЗ")
	fmt.Println("main импортирует zeta, alpha, mid (в этом порядке); mid импортирует zeta")
	gorun("go run ./initdeps", "./initdeps")
	bin := filepath.Join(work, "initdeps")
	if r := goCmd("build", "-o", bin, "./initdeps"); r.code != 0 {
		panic(r.out)
	}
	tr := runSplit([]string{"GODEBUG=inittrace=1"}, bin)
	reInit := regexp.MustCompile(`^init (\S+) @`)
	var std, ours []string
	for _, l := range strings.Split(tr.err, "\n") {
		m := reInit.FindStringSubmatch(l)
		if m == nil {
			continue
		}
		if strings.HasPrefix(m[1], mod) || m[1] == "main" {
			ours = append(ours, strings.TrimPrefix(m[1], mod))
		} else {
			std = append(std, m[1])
		}
	}
	deps := goCmd("list", "-deps", "./initdeps")
	nDeps := len(strings.Fields(deps.out))
	fmt.Println("GODEBUG=inittrace=1 ./initdeps   (пакеты с работой инициализации, по порядку)")
	fmt.Printf("    стандартная библиотека, %d: %s\n", len(std), strings.Join(std, " "))
	fmt.Printf("    наши, %d: %s\n", len(ours), strings.Join(ours, " "))
	fmt.Printf("    всего пакетов в сборке (go list -deps): %d\n", nDeps)

	// ------------------------------------------------------------------ 10
	show("10) init НЕЛЬЗЯ ВЫЗВАТЬ")
	build("go build ./initcall   (main вызывает init())", "./initcall")

	// ------------------------------------------------------------------ 11
	show("11) ЦИКЛ ИМПОРТОВ")
	build("go build ./cycle/orders   (orders -> users -> orders)", "./cycle/orders")
	build("go build ./cycle/self   (пакет импортирует сам себя)", "./cycle/self")

	// ------------------------------------------------------------------ 12
	show("12) ПУСТОЙ ИМПОРТ РАДИ init")
	gorun("go run ./blank/nopng   (импортирован только image)", "./blank/nopng")
	gorun("go run ./blank/withpng   (плюс import _ \"image/png\")", "./blank/withpng")
	build("go build ./blank/unused   (import \"image/png\" без _ и без обращений)", "./blank/unused")
	gorun("go run ./blank/nodriver   (только database/sql)", "./blank/nodriver")
	gorun("go run ./blank/withdriver   (плюс import _ \".../blank/fakedb\")", "./blank/withdriver")

	// ------------------------------------------------------------------ 13
	show("13) РАСКЛАДКА: cmd/ — СОГЛАШЕНИЕ, А НЕ ПРАВИЛО")
	outDir := filepath.Join(work, "bin") + string(os.PathSeparator)
	os.MkdirAll(outDir, 0o755)
	r = goCmd("build", "-o", outDir, "./layout/cmd/greet", "./layout/tools/anywhere")
	if r.code != 0 {
		panic(r.out)
	}
	bins, _ := os.ReadDir(outDir)
	var bn []string
	for _, b := range bins {
		bn = append(bn, b.Name())
	}
	fmt.Println("go build -o <tmp>/bin/ ./layout/cmd/greet ./layout/tools/anywhere")
	fmt.Println("    бинарники: " + strings.Join(bn, " "))
	gorun("go run ./layout/tools/anywhere", "./layout/tools/anywhere")
	build("go build ./layout/importmain   (импортирует пакет main из cmd/greet)", "./layout/importmain")

	// ------------------------------------------------------------------ 14
	// Полный пример урока — отдельный модуль cases/example со своим go.mod:
	// его можно скопировать в пустой каталог и запустить той же командой.
	show("14) ПОЛНЫЙ ПРИМЕР: ДВА ПАКЕТА В ОДНОМ МОДУЛЕ")
	ex := filepath.Join(cases, "example")
	exCmd := func(args ...string) string {
		cmd := exec.Command(goBin, args...)
		cmd.Dir = ex
		cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS=")
		out, err := cmd.CombinedOutput()
		if err != nil {
			panic(string(out))
		}
		return string(out)
	}
	fmt.Println("cd cases/example && go list ./...")
	lines(exCmd("list", "./..."), "    ")
	fmt.Println("cd cases/example && go run .")
	lines(exCmd("run", "."), "    ")
}