Пакеты и модули
Организуем код правильно: создаём модули, разбиваем проект на пакеты, управляем видимостью
До сих пор все наши программы умещались в один файл main.go. Для учебных примеров этого хватало, но реальные проекты — это десятки, сотни и тысячи файлов. Если свалить весь код в один файл, он превратится в неуправляемую кашу. Go решает эту проблему с помощью пакетов и модулей — элегантной системы организации кода, которая одновременно проста для понимания и мощна в работе. Сегодня мы научимся создавать собственные пакеты, разбивать код по файлам, управлять видимостью функций и переменных, а также познакомимся с богатейшей стандартной библиотекой Go.
📦 Что такое пакет в Go
Пакет (package) — это группа файлов с исходным кодом, которые лежат в одной папке и решают одну общую задачу. Каждый файл Go начинается с объявления пакета:
package main
Вы видели эту строку во всех наших программах. package main — это специальный пакет: именно из него Go запускает выполнение программы, вызывая функцию main(). Но помимо main вы можете создавать свои пакеты с любыми именами.
Представьте пакет как ящик с инструментами. В одном ящике — молотки и гвозди (пакет для работы с базой данных), в другом — кисти и краски (пакет для обработки изображений), в третьем — отвёртки (пакет для работы со строками). Каждый ящик самодостаточен и отвечает за свою область.
Правила именования пакетов:
• Имя пакета — строчные буквы, без подчёркиваний и camelCase: math, fmt, strings
• Имя должно быть коротким и понятным: user, а не userManagementSystem
• Имя пакета совпадает с именем папки, в которой лежат файлы
• Все файлы в одной папке обязаны принадлежать одному и тому же пакету
Вот простой пример: если у вас есть папка math, то все файлы в ней должны начинаться с package math. Нельзя положить в одну папку файл с package math и файл с package utils — компилятор выдаст ошибку.
💡 Подсказка: не называйте свои пакеты util, common, helpers — такие имена ничего не говорят о содержимом. Хорошее имя пакета описывает, что он делает, а не как он используется.
🛠 go mod init — создание модуля
Прежде чем создавать пакеты, нужно создать модуль. Модуль — это весь ваш проект целиком. Он объединяет все пакеты и хранит информацию о зависимостях (внешних библиотеках, которые вы используете).
Создаётся модуль одной командой в терминале:
mkdir myproject
cd myproject
go mod init myproject
После выполнения go mod init в корне проекта появится файл go.mod:
module myproject
go 1.22
Этот файл содержит две строки. Первая — имя модуля (myproject). Оно будет использоваться при импорте пакетов. Вторая — версия Go, которую вы используете.
Для реальных проектов, которые вы публикуете на GitHub, принято указывать полный путь к репозиторию:
go mod init github.com/username/myproject
Но для учёбы подойдёт и просто название проекта.
💡 Подсказка: файл go.mod — это как package.json в JavaScript или requirements.txt в Python. Он описывает ваш проект и его зависимости. Никогда не редактируйте его вручную — Go сам управляет его содержимым.
📁 Разбиваем код по файлам и пакетам
Давайте создадим реальный проект с несколькими пакетами. Представьте, что мы пишем программу-калькулятор. Вот как будет выглядеть структура папок:
calculator/
├── go.mod
├── main.go
└── mathops/
└── operations.go
У нас есть корневая папка calculator с файлом main.go (пакет main) и вложенная папка mathops с файлом operations.go (пакет mathops). Давайте создадим этот проект шаг за шагом.
Шаг 1. Инициализируем модуль:
mkdir calculator
cd calculator
go mod init calculator
Шаг 2. Создаём пакет mathops. Для этого создаём папку и файл внутри неё:
mkdir mathops
Файл mathops/operations.go:
package mathops
// Add складывает два числа
func Add(a, b float64) float64 {
return a + b
}
// Subtract вычитает b из a
func Subtract(a, b float64) float64 {
return a - b
}
// Multiply умножает два числа
func Multiply(a, b float64) float64 {
return a * b
}
// Divide делит a на b
func Divide(a, b float64) float64 {
if b == 0 {
return 0 // Защита от деления на ноль
}
return a / b
}
Шаг 3. Используем наш пакет в main.go:
package main
import (
"fmt"
"calculator/mathops"
)
func main() {
a := 20.0
b := 5.0
fmt.Printf("%.0f + %.0f = %.0f\n", a, b, mathops.Add(a, b))
fmt.Printf("%.0f - %.0f = %.0f\n", a, b, mathops.Subtract(a, b))
fmt.Printf("%.0f * %.0f = %.0f\n", a, b, mathops.Multiply(a, b))
fmt.Printf("%.0f / %.0f = %.0f\n", a, b, mathops.Divide(a, b))
}
Запускаем:
go run .
20 + 5 = 25
20 - 5 = 15
20 * 5 = 100
20 / 5 = 4
Обратите внимание на путь импорта: "calculator/mathops". Он состоит из имени модуля (calculator, которое мы задали в go mod init) и имени папки с пакетом (mathops). Go именно по этому пути находит нужный код.
А вызываем функции через имя пакета: mathops.Add(), mathops.Subtract() и так далее. Пакет — это как пространство имён, которое не даёт функциям из разных пакетов конфликтовать друг с другом.
💡 Подсказка: команда go run . (с точкой) компилирует и запускает все файлы пакета main в текущей директории. Это удобнее, чем перечислять файлы вручную: go run main.go.
📄 Несколько файлов в одном пакете
Один пакет может состоять из нескольких файлов. Все файлы в одной папке «видят» друг друга — они ведут себя так, будто весь код написан в одном файле. Давайте расширим наш пакет mathops:
calculator/
├── go.mod
├── main.go
└── mathops/
├── operations.go
└── power.go
Файл mathops/power.go:
package mathops
// Square возвращает квадрат числа
func Square(x float64) float64 {
return Multiply(x, x) // Используем функцию из operations.go!
}
// Cube возвращает куб числа
func Cube(x float64) float64 {
return Multiply(Multiply(x, x), x)
}
Обратите внимание: файл power.go свободно вызывает функцию Multiply из файла operations.go. Никакого импорта не нужно — они в одном пакете! Теперь в main.go мы можем использовать все функции:
package main
import (
"fmt"
"calculator/mathops"
)
func main() {
fmt.Println(mathops.Square(7)) // 49
fmt.Println(mathops.Cube(3)) // 27
fmt.Println(mathops.Add(10, 5)) // 15
}
Разделять код на файлы удобно для читаемости. Обычно в один файл помещают логически связанные функции. Например: operations.go — базовые операции, power.go — степени, trig.go — тригонометрия.
🔓 Публичные и приватные идентификаторы
В Go есть простое и гениальное правило видимости. Никаких ключевых слов public, private или protected, как в других языках. Всё решает первая буква имени:
Заглавная буква — идентификатор экспортирован (публичный). Доступен из любого пакета.
Строчная буква — идентификатор не экспортирован (приватный). Доступен только внутри своего пакета.
Это правило работает для всего: функций, переменных, констант, типов, полей структур. Давайте посмотрим на примере:
Файл mathops/operations.go:
package mathops
// Add — экспортированная функция (с заглавной буквы)
// Доступна из других пакетов: mathops.Add(1, 2)
func Add(a, b float64) float64 {
return a + b
}
// validate — НЕэкспортированная функция (со строчной буквы)
// Доступна ТОЛЬКО внутри пакета mathops
func validate(x float64) bool {
return x >= 0
}
// MaxValue — экспортированная константа
const MaxValue = 1_000_000
// pi — НЕэкспортированная константа
const pi = 3.14159265
Теперь посмотрим, что произойдёт, если мы попробуем обратиться к приватным идентификаторам из другого пакета:
package main
import (
"fmt"
"calculator/mathops"
)
func main() {
fmt.Println(mathops.Add(1, 2)) // ОК — Add экспортирована
fmt.Println(mathops.MaxValue) // ОК — MaxValue экспортирована
// fmt.Println(mathops.validate(5)) // ОШИБКА КОМПИЛЯЦИИ!
// fmt.Println(mathops.pi) // ОШИБКА КОМПИЛЯЦИИ!
}
Компилятор Go выдаст ошибку: cannot refer to unexported name mathops.validate. Этот механизм — инкапсуляция. Вы скрываете внутренние детали реализации и показываете наружу только то, что нужно пользователям вашего пакета.
Это правило работает и для полей структур:
package user
type User struct {
Name string // Экспортировано — доступно снаружи
Email string // Экспортировано — доступно снаружи
age int // НЕ экспортировано — только внутри пакета user
}
// NewUser — конструктор, позволяющий задать все поля
func NewUser(name, email string, age int) User {
return User{
Name: name,
Email: email,
age: age, // Внутри пакета мы имеем полный доступ
}
}
// Age — геттер для приватного поля
func (u User) Age() int {
return u.age
}
Использование из другого пакета:
package main
import (
"fmt"
"myproject/user"
)
func main() {
u := user.NewUser("Алексей", "alex@example.com", 25)
fmt.Println(u.Name) // ОК — поле экспортировано
fmt.Println(u.Email) // ОК — поле экспортировано
// fmt.Println(u.age) // ОШИБКА — поле не экспортировано
fmt.Println(u.Age()) // ОК — используем геттер
}
💡 Подсказка: в Go принято использовать паттерн «конструктор» — экспортированная функция NewXxx(), которая создаёт и возвращает структуру. Это позволяет проверять данные перед созданием объекта и инициализировать приватные поля.
📥 Тонкости импорта пакетов
Мы уже использовали import во всех программах. Давайте разберём все возможности импорта подробнее.
Импорт одного пакета:
import "fmt"
Импорт нескольких пакетов — используем группировку в скобках:
import (
"fmt"
"strings"
"math"
)
Импорт с псевдонимом — если имя пакета неудобное или конфликтует с другим:
import (
"fmt"
m "math" // Теперь пакет math доступен как m
)
func main() {
fmt.Println(m.Sqrt(16)) // 4
}
Важное правило Go: если вы импортировали пакет, но не используете его — программа не скомпилируется! Go заставляет вас поддерживать чистый код.
import (
"fmt"
"math" // ОШИБКА: imported and not used: "math"
)
func main() {
fmt.Println("hello")
// math нигде не используется — компилятор не пропустит
}
💡 Подсказка: если вы используете VS Code с расширением Go или GoLand, редактор автоматически добавляет и убирает импорты при сохранении файла. Инструмент goimports делает это за вас — настройте его, и вы забудете о ручном управлении импортами.
🏋️ Практика: проект с несколькими пакетами
Давайте создадим проект «Менеджер задач», в котором будет несколько пакетов. Это даст полную картину того, как организовать реальный проект на Go.
taskmanager/
├── go.mod
├── main.go
├── task/
│ └── task.go
└── storage/
└── storage.go
Инициализируем модуль:
go mod init taskmanager
Файл task/task.go — определяет структуру задачи:
package task
import "fmt"
// Task представляет одну задачу
type Task struct {
ID int
Title string
Done bool
}
// String возвращает строковое представление задачи
func (t Task) String() string {
status := "[ ]"
if t.Done {
status = "[x]"
}
return fmt.Sprintf("%s #%d: %s", status, t.ID, t.Title)
}
// Complete отмечает задачу как выполненную
func (t *Task) Complete() {
t.Done = true
}
Файл storage/storage.go — хранилище задач:
package storage
import "taskmanager/task"
// Storage хранит все задачи
type Storage struct {
tasks []task.Task // приватное поле — снаружи не изменить напрямую
nextID int // приватный счётчик ID
}
// New создаёт новое хранилище
func New() *Storage {
return &Storage{
tasks: []task.Task{},
nextID: 1,
}
}
// Add добавляет новую задачу и возвращает её
func (s *Storage) Add(title string) task.Task {
t := task.Task{
ID: s.nextID,
Title: title,
Done: false,
}
s.tasks = append(s.tasks, t)
s.nextID++
return t
}
// All возвращает все задачи
func (s *Storage) All() []task.Task {
return s.tasks
}
// Complete отмечает задачу как выполненную по ID
func (s *Storage) Complete(id int) bool {
for i := range s.tasks {
if s.tasks[i].ID == id {
s.tasks[i].Complete()
return true
}
}
return false
}
Файл main.go — точка входа:
package main
import (
"fmt"
"taskmanager/storage"
)
func main() {
store := storage.New()
// Добавляем задачи
store.Add("Изучить пакеты в Go")
store.Add("Написать свой проект")
store.Add("Пройти следующий урок")
// Выполняем первую задачу
store.Complete(1)
// Выводим все задачи
fmt.Println("=== Мои задачи ===")
for _, t := range store.All() {
fmt.Println(t)
}
}
Результат:
=== Мои задачи ===
[x] #1: Изучить пакеты в Go
[ ] #2: Написать свой проект
[ ] #3: Пройти следующий урок
Посмотрите, как красиво разделены обязанности: пакет task знает только о самой задаче, пакет storage умеет хранить задачи и управлять ими, а main просто использует оба пакета. Каждый пакет можно развивать и тестировать отдельно.
📚 Стандартная библиотека Go
Одна из сильнейших сторон Go — богатая стандартная библиотека. Это набор пакетов, которые идут «из коробки» и не требуют установки. В них уже реализовано огромное количество полезных вещей: от работы со строками до создания веб-серверов.
Вот самые важные пакеты, которые вы будете использовать постоянно:
fmt — форматированный ввод/вывод. Println, Printf, Sprintf, Scanf.
strings — работа со строками: поиск, замена, разбиение, склейка.
strconv — преобразование строк в числа и обратно.
math — математические функции: корень, степень, округление, тригонометрия.
sort — сортировка срезов.
os — работа с операционной системой: файлы, переменные окружения, аргументы командной строки.
io — интерфейсы для чтения и записи данных.
errors — создание и обработка ошибок (подробно в следующем уроке!).
time — работа с датой и временем.
net/http — HTTP-клиент и сервер. Можно поднять веб-сервер в 5 строк!
encoding/json — работа с JSON: чтение и запись.
Давайте посмотрим некоторые из этих пакетов в действии.
Пакет strings — работа со строками:
package main
import (
"fmt"
"strings"
)
func main() {
text := "Go - отличный язык программирования"
fmt.Println(strings.ToUpper(text)) // GO - ОТЛИЧНЫЙ ЯЗЫК ПРОГРАММИРОВАНИЯ
fmt.Println(strings.Contains(text, "Go")) // true
fmt.Println(strings.ReplaceAll(text, "Go", "Golang")) // Golang - отличный язык...
// Разбиваем строку по разделителю
csv := "яблоко,банан,вишня"
fruits := strings.Split(csv, ",")
fmt.Println(fruits) // [яблоко банан вишня]
// Склеиваем обратно
result := strings.Join(fruits, " и ")
fmt.Println(result) // яблоко и банан и вишня
}
Пакет strconv — преобразование типов:
package main
import (
"fmt"
"strconv"
)
func main() {
// Строка в число
num, err := strconv.Atoi("42")
if err == nil {
fmt.Println(num + 8) // 50
}
// Число в строку
str := strconv.Itoa(100)
fmt.Println("Ответ: " + str) // Ответ: 100
// Строка в float64
pi, err := strconv.ParseFloat("3.14", 64)
if err == nil {
fmt.Println(pi * 2) // 6.28
}
}
Пакет time — дата и время:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println("Сейчас:", now.Format("02.01.2006 15:04"))
birthday := time.Date(2000, time.March, 15, 0, 0, 0, 0, time.UTC)
age := now.Sub(birthday)
fmt.Printf("Возраст: %.0f дней\n", age.Hours()/24)
}
Пакет os — работа с файлами:
package main
import (
"fmt"
"os"
)
func main() {
// Читаем содержимое файла
data, err := os.ReadFile("hello.txt")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println(string(data))
// Аргументы командной строки
args := os.Args
fmt.Println("Программа:", args[0])
if len(args) > 1 {
fmt.Println("Аргументы:", args[1:])
}
}
💡 Подсказка: полную документацию по стандартной библиотеке Go можно найти на pkg.go.dev/std. Там описан каждый пакет с примерами. Это ваш главный справочник.
⚠️ Частые ошибки новичков
Разберём типичные ловушки, связанные с пакетами и модулями.
Ошибка 1: забыли go mod init
// Если нет go.mod, при попытке импорта своих пакетов получим:
// cannot find module providing package myproject/mathops
Решение: всегда начинайте проект с go mod init имя_модуля.
Ошибка 2: неправильный путь импорта
// go.mod: module myproject
// НЕПРАВИЛЬНО:
import "mathops" // Нет имени модуля!
// ПРАВИЛЬНО:
import "myproject/mathops" // Имя модуля + имя папки
Решение: путь импорта всегда начинается с имени модуля из go.mod.
Ошибка 3: обращение к приватному идентификатору
// В пакете mathops:
func add(a, b int) int { ... } // строчная буква — приватная!
// В main.go:
mathops.add(1, 2) // ОШИБКА: cannot refer to unexported name
Решение: если функция должна быть доступна снаружи, начните её имя с заглавной буквы — Add.
Ошибка 4: разные пакеты в одной папке
// math/add.go: package math
// math/sub.go: package calculator // ОШИБКА! Должно быть package math
Решение: все файлы в одной папке должны объявлять один и тот же пакет.
📝 Шпаргалка
Все ключевые концепции урока в одном месте:
// === МОДУЛЬ ===
go mod init myproject // Создать модуль
// Файл go.mod появится в корне проекта
// === ПАКЕТЫ ===
package main // Специальный пакет — точка входа
package mathops // Пользовательский пакет
// === ИМПОРТ ===
import "fmt" // Стандартный пакет
import "myproject/mathops" // Свой пакет: модуль/папка
import m "math" // Импорт с псевдонимом
// === ВИДИМОСТЬ ===
func Add() {} // Экспортировано (заглавная) — доступно снаружи
func add() {} // Не экспортировано (строчная) — только внутри пакета
var Count int // Экспортировано
var count int // Не экспортировано
// === ЗАПУСК ===
go run . // Запустить все файлы пакета main
go build . // Собрать исполняемый файл
📋 Итоги урока
- Пакет — это группа файлов в одной папке, объединённых общей задачей
- Имя пакета пишется строчными буквами и совпадает с именем папки
- Команда
go mod initсоздаёт модуль — корневую единицу проекта с файломgo.mod - Путь импорта своего пакета:
"имя_модуля/имя_папки" - Файлы в одном пакете видят друг друга без импорта
- Заглавная буква — экспортировано (публичное), строчная — не экспортировано (приватное)
- Это правило работает для функций, переменных, констант, типов и полей структур
- Стандартная библиотека Go содержит пакеты для большинства повседневных задач:
fmt,strings,strconv,os,time,net/http,encoding/json - Неиспользуемый импорт — ошибка компиляции; Go требует чистоту кода
В следующем уроке мы разберём обработку ошибок — одну из самых характерных особенностей Go. В отличие от большинства языков, Go не использует исключения (try/catch). Вместо этого функции возвращают ошибку как обычное значение, и программист обязан её проверить. Вы узнаете, как создавать, возвращать и обрабатывать ошибки, познакомитесь с пакетом errors и научитесь писать надёжный код, который корректно реагирует на любые проблемы.