Любая программа на Go состоит из пакетов. Даже крошечный Hello, world —
это пакет main. Как только кода становится больше пары файлов, встаёт вопрос:
как его разложить, чтобы не утонуть? Ответ Go прост и строг — код группируют по
пакетам, а пакеты живут внутри модуля.
В этой главе разберём три уровня этой системы. Пакет — папка с файлами,
которые работают вместе. Импорт — как один пакет берёт код другого.
Модуль — единица, которой пакеты распространяются и версионируются, со своим
файлом go.mod. Заодно поймём, почему Println пишут с большой буквы, а
fmt.Sprintf доступен из коробки.
Сразу честно: большинство примеров здесь статичны. Многофайловый проект,
go mod init и несколько пакетов в одной программе невозможно показать в
браузерном playground — он умеет запускать только одиночный package main.
Поэтому структуру модуля мы нарисуем деревом и разберём словами, а запускать
будем то, что укладывается в один файл: использование стандартной библиотеки.
Пакет — это просто папка с .go-файлами, у которых в первой строке одно и
то же объявление package. Все файлы одного пакета делят общее пространство
имён: функция, объявленная в одном файле, видна в другом без всякого импорта.
Делить пакет на несколько файлов — нормально и полезно: складываете связанные
части по смыслу.
// файл user.gopackage accounttype User struct { Name string}
// файл balance.go — тот же пакет account, другой файлpackage accountfunc NewUser(name string) *User { return &User{Name: name} // User виден без импорта: тот же пакет}
Имя пакета по соглашению короткое, в нижнем регистре, без подчёркиваний:
account, http, strconv. Обычно оно совпадает с именем папки — так проще
ориентироваться. Особый случай — пакет main: он не библиотека, а точка входа.
Именно в нём живёт func main(), с которой стартует исполняемая программа.
Чтобы воспользоваться кодом другого пакета, его импортируют. Один импорт
пишут в строку, несколько — группируют в блок import ( ... ):
import ( "fmt" "strings")
В кавычках указывают путь импорта — это не имя пакета, а его адрес. Для
стандартной библиотеки путь короткий: fmt, strings, math/rand. Для чужого
кода путь обычно начинается с домена репозитория, например
github.com/google/uuid. А обращаются к содержимому всегда по имени пакета
(последний элемент пути): импортировав math/rand, вызываете rand.Intn(...).
Go придирчив к импортам: неиспользуемый импорт — это ошибка компиляции, а не
предупреждение. Сначала раздражает, потом привыкаешь — лишних зависимостей в
коде не накапливается. Инструмент gofmt/goimports сам сортирует блок и
убирает мусор, так что вручную за этим следить почти не приходится.
Иногда удобно дать импорту псевдоним — например, когда два пакета называются
одинаково или имя слишком длинное:
import ( crand "crypto/rand" // теперь обращаемся как crand.Read(...) "math/rand" // а это просто rand)
Вот центральное правило Go про видимость, и оно поразительно простое.
Идентификатор экспортируется (виден снаружи пакета) тогда и только тогда,
когда начинается с заглавной буквы. С маленькой — он приватный, доступен лишь
внутри своего пакета.
package accounttype User struct { Name string // экспортируется: видно снаружи balance int // с маленькой — приватное поле, снаружи недоступно}func New(name string) *User { ... } // New виден другим пакетамfunc validate(name string) bool { ... } // validate — только внутри account
Никаких ключевых слов public/private — регистр первой буквы и есть модификатор
доступа. Это работает для всего: функций, типов, методов, полей структур,
констант, переменных. Поэтому fmt.Println пишется с большой P (его вызывают
извне), а внутренние помощники пакета fmt — с маленькой, и вы их просто не
видите.
Практический смысл: то, что с заглавной, — это публичный контракт пакета,
его API. То, что с маленькой, — внутренняя кухня, которую автор волен менять, не
ломая пользователей. Когда проектируете пакет, делайте экспортируемым только
необходимый минимум: чем меньше публичная поверхность, тем легче её
поддерживать.
Пакеты не висят в воздухе — они принадлежат модулю. Модуль — это дерево
пакетов, которые версионируются и распространяются как одно целое. У модуля есть
корневая папка и в ней файл go.mod, который объявляет путь модуля и его
зависимости.
Создаётся модуль одной командой в корне проекта:
go mod init github.com/me/shop
Она генерирует минимальный go.mod:
module github.com/me/shopgo 1.24
Первая строка — путь модуля. Он становится префиксом для путей импорта всех
пакетов внутри. Если в модуле github.com/me/shop есть папка cart, то её
пакет импортируют как github.com/me/shop/cart. Так Go однозначно понимает, где
искать код, и не путает ваш cart с чужим.
Когда вы добавляете внешнюю зависимость и запускаете go mod tidy, Go дописывает
её в go.mod в блок require и фиксирует точные версии в файле-«замке»
go.sum. Вдвоём эти файлы делают сборку воспроизводимой: у вас и у коллеги
скачаются ровно одинаковые версии зависимостей.
main.go в корне — это package main с func main(). Точка входа,
которую собирают в исполняемый файл командой go build.
cart/ — обычный библиотечный пакет. Файл cart.go начинается с
package cart; из main.go его берут импортом
github.com/me/shop/cart.
cart_test.go лежит рядом с кодом, который тестирует, — в Go тесты
держат в том же пакете, в файлах с суффиксом _test.go.
internal/ — особая папка. Пакеты под ней может импортировать только код
из того же модуля. Это встроенный в Go способ сказать «это деталь реализации,
снаружи трогать нельзя».
Главный принцип группировки: по смыслу, а не по типу. Не делайте папки
models, services, utils со свалкой всего подряд. Лучше пакет cart, в
котором и тип корзины, и операции над ней. Имя пакета — часть API: вызов
cart.Add(...) читается лучше, чем utils.AddToCart(...).
Go приходит с большой стандартной библиотекой — набором пакетов, доступных
без скачивания чего-либо. Вы их уже встречали; вот ориентир по самым ходовым:
strings — операции над строками: Contains, Split, ToUpper, Join.
strconv — преобразования строк и чисел: Atoi, Itoa, ParseFloat.
sort — сортировка срезов и пользовательских коллекций.
errors — создание и разбор ошибок: New, Is, As.
time — время, длительности, таймеры.
math, math/rand — математика и псевдослучайные числа.
os — аргументы, переменные окружения, работа с файлами (в браузере недоступен).
Прелесть в том, что всё это — обычные пакеты: импортируете по короткому пути и
обращаетесь по имени. Никакой разницы с вашими собственными пакетами, кроме того
что устанавливать ничего не нужно.
Соберём небольшую программу, которая задействует сразу три пакета стандартной
библиотеки: strings разбивает строку, strconv превращает текст в числа, а
sort их упорядочивает.
package mainimport ( "fmt" "sort" "strconv" "strings")func main() { raw := "42, 7, 13, 99, 1" parts := strings.Split(raw, ",") // ["42", " 7", ...] nums := make([]int, 0, len(parts)) for _, p := range parts { n, _ := strconv.Atoi(strings.TrimSpace(p)) // текст -> число nums = append(nums, n) } sort.Ints(nums) // сортируем по возрастанию fmt.Println("отсортировано:", nums) fmt.Println("минимум:", nums[0], "максимум:", nums[len(nums)-1])}
Обратите внимание, как естественно пакеты складываются друг с другом:
strings.TrimSpace чистит лишние пробелы перед тем, как strconv.Atoi разберёт
число. Каждый пакет делает что-то одно и делает хорошо — это и есть дух
стандартной библиотеки.
Ещё один частый герой — strings.Builder для эффективной сборки строки по
кусочкам, плюс fmt.Sprintf, который форматирует значение в строку, ничего не
печатая:
package mainimport ( "fmt" "strings")func main() { var b strings.Builder for i := 1; i <= 3; i++ { // Sprintf форматирует в строку и возвращает её b.WriteString(fmt.Sprintf("[%d]", i)) } line := b.String() fmt.Println("собрано:", line) fmt.Println("в верхнем регистре:", strings.ToUpper(line))}
Вывод:
собрано: [1][2][3]в верхнем регистре: [1][2][3]
(ToUpper не тронул цифры и скобки — менять там нечего, поэтому строка осталась
прежней. Зато на буквах разница была бы видна.)
Путают путь импорта и имя пакета. В кавычках пишут адрес
(math/rand), а обращаются по последнему элементу (rand.Intn). Если имя
пакета внутри файла отличается от папки — ориентируйтесь на строку package,
именно она задаёт, как к пакету обращаться.
Забывают про заглавную букву. Объявили func parseConfig(...) с маленькой
и удивляются, почему из другого пакета его не видно. Хотите экспортировать —
ParseConfig. И наоборот: не делайте публичным то, что должно быть внутренним.
Неиспользуемый импорт. Go не соберёт код с импортом, который вы не
применяете. Удалите строку (или дайте goimports сделать это за вас). Если
пакет нужен только ради его побочного эффекта — есть приём import _ "путь",
но это редкий случай.
Свалка в utils. Один гигантский пакет «на всё» быстро превращается в
клубок зависимостей. Группируйте по смыслу и держите публичный API узким.
Теперь у вас есть карта: код живёт в пакетах, видимость задаёт регистр
первой буквы, а пакеты собираются в модуль с go.mod. Этого достаточно,
чтобы читать чужие проекты и раскладывать свой собственный.
Стандартная библиотека — ваш главный союзник: прежде чем тянуть внешнюю
зависимость, загляните, нет ли нужного пакета уже в комплекте. Дальше эти знания
понадобятся, когда дойдём до тестирования (файлы _test.go
живут прямо рядом с кодом) и до обработки ошибок, где пакет
errors и идиома value, err раскроются полностью.