Deep Engineering

ЗАМЕР

bench/gopkg/practice.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. Тексты диагностик и число пакетов в графе сборки — наблюдения этих версий, а не правила языка.

Скрипт

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

// Две задачи урока про пакеты и один факультативный эксперимент: всё
// отвечает прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Программа из двух пакетов: main импортирует
// dep, в main переменные объявлены не в том порядке, в каком зависят друг от
// друга, и функций init две. Вывод отвечает на три вопроса сразу: кто раньше —
// зависимость или импортирующий пакет; в каком порядке инициализируются
// переменные; когда вызываются init. Программа записывается во временный
// каталог из строк ниже и запускается командой go — поэтому показанный в
// уроке код дословно есть в этом файле.
//
// ЭКСПЕРИМЕНТ (раздел 2, не задача). Сколько пакетов войдёт в программу,
// которая импортирует только fmt, и сколько из них делают работу
// инициализации. Число зависит от версии Go и конфигурации сборки, поэтому
// печатается вместе с ней: это не знание о языке, а наблюдение.
//
// ЗАДАЧА 2 (раздел 3) — ОЦЕНИТЬ ЧИСЛО. Пакеты a, b и c импортируют общий
// пакет reg, main импортирует a, b и c; в каждом из пяти пакетов одна init,
// которая печатает строку. Сколько строк init будет до main? Ответ держится
// на правиле «каждый пакет инициализируется один раз», а не на памяти.
//
// ЗАПУСК:
//
//	go run bench/gopkg/practice.go
//	/usr/local/go1.25.1/bin/go run bench/gopkg/practice.go
package main

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

// ---------------------------------------------------------------- задача 1

const goMod = `module example

go 1.24
`

const depGo = `
// 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") }
`

const mainGo = `
// 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) }
`

// ---------------------------------------------------------------- эксперимент

const helloGo = `package main

import "fmt"

func main() { fmt.Println("hello") }
`

// ---------------------------------------------------------------- задача 2

const regGo = `package reg

import "fmt"

func init() { fmt.Println("init reg") }

func Touch() {}
`

// leafGo — пакеты a, b и c: каждый импортирует reg.
const leafGo = `package NAME

import (
	"fmt"

	"example/reg"
)

func init() {
	reg.Touch()
	fmt.Println("init NAME")
}
`

const shareMain = `package main

import (
	"fmt"

	_ "example/a"
	_ "example/b"
	_ "example/c"
)

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

func main() { fmt.Println("main") }
`

var goBin string

func write(dir, name, body string) {
	p := filepath.Join(dir, name)
	if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
		panic(err)
	}
	if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
		panic(err)
	}
}

func command(dir string, env []string, name string, args ...string) (stdout, stderr string) {
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	cmd.Env = append(append(os.Environ(), "GOTOOLCHAIN=local", "GOFLAGS="), env...)
	var so, se strings.Builder
	cmd.Stdout, cmd.Stderr = &so, &se
	if err := cmd.Run(); err != nil {
		panic(fmt.Sprint(err, se.String()))
	}
	return so.String(), se.String()
}

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

func main() {
	goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
	if _, err := os.Stat(goBin); err != nil {
		goBin = "go"
	}
	work, err := os.MkdirTemp("", "gopkg-practice")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(work)

	fmt.Printf("%s %s/%s | пакеты: задачи урока и эксперимент\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)

	show("1) ПОРЯДОК ИНИЦИАЛИЗАЦИИ: ВЫВОД ПРОГРАММЫ")
	p1 := filepath.Join(work, "p1")
	write(p1, "go.mod", goMod)
	write(p1, "dep/dep.go", depGo)
	write(p1, "main.go", mainGo)
	out, _ := command(p1, nil, goBin, "run", ".")
	fmt.Print(out)

	show("2) ЭКСПЕРИМЕНТ: СКОЛЬКО ПАКЕТОВ В ГРАФЕ СБОРКИ")
	p2 := filepath.Join(work, "p2")
	write(p2, "go.mod", goMod)
	write(p2, "main.go", helloGo)
	deps, _ := command(p2, nil, goBin, "list", "-deps", ".")
	n := len(strings.Fields(deps))
	bin := filepath.Join(work, "hello")
	command(p2, nil, goBin, "build", "-o", bin, ".")
	_, trace := command(p2, []string{"GODEBUG=inittrace=1"}, bin)
	re := regexp.MustCompile(`(?m)^init (\S+) @`)
	var withWork []string
	for _, m := range re.FindAllStringSubmatch(trace, -1) {
		withWork = append(withWork, m[1])
	}
	env, _ := command(p2, nil, goBin, "env", "GOOS", "GOARCH", "CGO_ENABLED")
	fmt.Println("программа: package main, import \"fmt\", fmt.Println(\"hello\")")
	fmt.Printf("  конфигурация: %s, GOOS/GOARCH/CGO_ENABLED = %s, GOFLAGS пуст\n",
		runtime.Version(), strings.Join(strings.Fields(env), "/"))
	fmt.Printf("  пакетов в графе сборки (go list -deps, вместе с main и unsafe): %d\n", n)
	fmt.Printf("  из них с работой инициализации (GODEBUG=inittrace=1): %d\n", len(withWork))
	fmt.Printf("  %s\n", strings.Join(withWork, " "))

	show("3) ОБЩАЯ ЗАВИСИМОСТЬ: СКОЛЬКО СТРОК init ДО main")
	p3 := filepath.Join(work, "p3")
	write(p3, "go.mod", goMod)
	write(p3, "reg/reg.go", regGo)
	for _, name := range []string{"a", "b", "c"} {
		write(p3, name+"/"+name+".go", strings.ReplaceAll(leafGo, "NAME", name))
	}
	write(p3, "main.go", shareMain)
	out, _ = command(p3, nil, goBin, "run", ".")
	fmt.Println("a, b, c импортируют reg; main импортирует a, b, c; в каждом пакете одна init")
	fmt.Print(out)
	inits := 0
	for _, l := range strings.Split(out, "\n") {
		if strings.HasPrefix(l, "init ") {
			inits++
		}
	}
	fmt.Printf("  строк init до main: %d (пакетов: 5, импортов reg: 3)\n", inits)
}