GraphLMS

Основы
Начать
Глава 11 · Основы~12 мин чтения

Пакеты и модули

О чём глава

Любая программа на Go состоит из пакетов. Даже крошечный Hello, world — это пакет main. Как только кода становится больше пары файлов, встаёт вопрос: как его разложить, чтобы не утонуть? Ответ Go прост и строг — код группируют по пакетам, а пакеты живут внутри модуля.

В этой главе разберём три уровня этой системы. Пакет — папка с файлами, которые работают вместе. Импорт — как один пакет берёт код другого. Модуль — единица, которой пакеты распространяются и версионируются, со своим файлом go.mod. Заодно поймём, почему Println пишут с большой буквы, а fmt.Sprintf доступен из коробки.

Сразу честно: большинство примеров здесь статичны. Многофайловый проект, go mod init и несколько пакетов в одной программе невозможно показать в браузерном playground — он умеет запускать только одиночный package main. Поэтому структуру модуля мы нарисуем деревом и разберём словами, а запускать будем то, что укладывается в один файл: использование стандартной библиотеки.

Что такое пакет

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

// файл user.go
package account
 
type User struct {
    Name string
}
// файл balance.go — тот же пакет account, другой файл
package account
 
func 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 account
 
type 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, который объявляет путь модуля и его зависимости.

Создаётся модуль одной командой в корне проекта:

go mod init github.com/me/shop

Она генерирует минимальный go.mod:

module github.com/me/shop
 
go 1.24

Первая строка — путь модуля. Он становится префиксом для путей импорта всех пакетов внутри. Если в модуле github.com/me/shop есть папка cart, то её пакет импортируют как github.com/me/shop/cart. Так Go однозначно понимает, где искать код, и не путает ваш cart с чужим.

Когда вы добавляете внешнюю зависимость и запускаете go mod tidy, Go дописывает её в go.mod в блок require и фиксирует точные версии в файле-«замке» go.sum. Вдвоём эти файлы делают сборку воспроизводимой: у вас и у коллеги скачаются ровно одинаковые версии зависимостей.

Как раскладывают код по пакетам

Посмотрим, как это выглядит целиком. Типичный небольшой проект:

shop/
├── go.mod                      module github.com/me/shop
├── go.sum
├── main.go                     package main
├── cart/
│   ├── cart.go                 package cart
│   └── cart_test.go            package cart
└── internal/
    └── pricing/
        └── pricing.go          package pricing

Что здесь важно увидеть:

  • 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 приходит с большой стандартной библиотекой — набором пакетов, доступных без скачивания чего-либо. Вы их уже встречали; вот ориентир по самым ходовым:

  • fmt — форматированный ввод-вывод: Println, Printf, Sprintf.
  • strings — операции над строками: Contains, Split, ToUpper, Join.
  • strconv — преобразования строк и чисел: Atoi, Itoa, ParseFloat.
  • sort — сортировка срезов и пользовательских коллекций.
  • errors — создание и разбор ошибок: New, Is, As.
  • time — время, длительности, таймеры.
  • math, math/rand — математика и псевдослучайные числа.
  • os — аргументы, переменные окружения, работа с файлами (в браузере недоступен).

Прелесть в том, что всё это — обычные пакеты: импортируете по короткому пути и обращаетесь по имени. Никакой разницы с вашими собственными пакетами, кроме того что устанавливать ничего не нужно.

Соберём небольшую программу, которая задействует сразу три пакета стандартной библиотеки: strings разбивает строку, strconv превращает текст в числа, а sort их упорядочивает.

package main
 
import (
	"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])
}

Вывод:

отсортировано: [1 7 13 42 99]
минимум: 1 максимум: 99

Обратите внимание, как естественно пакеты складываются друг с другом: strings.TrimSpace чистит лишние пробелы перед тем, как strconv.Atoi разберёт число. Каждый пакет делает что-то одно и делает хорошо — это и есть дух стандартной библиотеки.

Ещё один частый герой — strings.Builder для эффективной сборки строки по кусочкам, плюс fmt.Sprintf, который форматирует значение в строку, ничего не печатая:

package main
 
import (
	"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 раскроются полностью.