$ sudo teach IT
МОДУЛЬ 4 · УРОК 4.1

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

Организуем код правильно: создаём модули, разбиваем проект на пакеты, управляем видимостью

До сих пор все наши программы умещались в один файл 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 и научитесь писать надёжный код, который корректно реагирует на любые проблемы.