Пакеты, модули и организация проекта
Привет! Двеннадцатый урок — про то, как писать настоящий, профессиональный код на Go. До этого мы уже изучали в первом уроке структуру проекта и в этом уроке мы это закрепим и научимся структурировать большие проекты, использовать пакеты, управлять зависимостями и следовать лучшим практикам.
Это то, что отличает хобби-проект от production-ready приложения.
Пакеты
Пакет — это папка с Go-файлами, имеющими одинаковое объявление package.
Правила экспорта
- Заглавная буква → экспортируется (доступна извне пакета)
- строчная буква → приватная (только внутри пакета)
// pkg/mathutils/add.go
package mathutils
func Add(a, b int) int { // экспортируется
return a + b
}
func secretHelper(x int) int { // приватная
return x * 2
}
// main.go
import "myproject/pkg/mathutils"
func main() {
mathutils.Add(3, 4) // OK
// mathutils.secretHelper(5) // ошибка компиляции
}
internal — приватные пакеты
Папка internal/ — код, который нельзя импортировать извне модуля.
myproject/
├── internal/
│ └── database/ // только мой проект может импортировать
└── pkg/
└── api/ // можно импортировать извне
init() — автоматическая инициализация
Функция init() вызывается автоматически при импорте пакета (можно несколько).
// internal/config/config.go
package config
import "log"
var AppConfig Config
func init() {
slog.Info("Конфигурация инициализируется...")
// загрузка из env, файла и т.д.
}
Модули (go.mod)
Модули — это способ организовать код проекта и его зависимости (внешние пакеты), чтобы всё было удобно, предсказуемо и повторяемо. С Go 1.11 появился файл go.mod, в котором ты указываешь название модуля (например, example.com/myapp) и версии нужных библиотек (require github.com/gin-gonic/gin v1.9.1).
Благодаря модулям Go сам скачивает нужные пакеты, проверяет их версии, решает конфликты и гарантирует, что твой проект соберётся одинаково на любом компьютере. Они нужны, чтобы не было "хаоса зависимостей", легко делиться кодом, обновлять библиотеки и избежать ситуации, когда у одного разработчика код работает, а у другого — нет из-за разных версий пакетов.
Модули — это "паспорт" твоего проекта, который управляет зависимостями и делает разработку надёжной и простой.
Инициализация модуля
go mod init github.com/ваш-ник/myproject
Создаст go.mod:
module github.com/ваш-ник/myproject
go 1.23
Добавление зависимостей
go get github.com/gin-gonic/gin@latest
go get github.com/lib/pq
go get github.com/joho/godotenv
go.mod обновится, появится go.sum с хешами.
Полезные команды
go mod tidy # удалить неиспользуемые, добавить недостающие
go mod vendor # скопировать зависимости в vendor/ (для старых систем)
go list -m all # список всех зависимостей
go mod why pkg # зачем нужен этот пакет
Рекомендуемая структура проекта
myproject/
├── go.mod
├── go.sum
├── cmd/ # Точки входа (исполняемые программы)
│ └── server/
│ └── main.go # HTTP-сервер
├── internal/ # Приватный код (не импортируется извне)
│ ├── config/ # Загрузка конфигурации
│ ├── database/ # Подключение к БД, репозитории
│ ├── api/ # HTTP-обработчики, маршруты
│ ├── service/ # Бизнес-логика
│ └── models/ # Структуры данных
├── pkg/ # Публичные пакеты (можно переиспользовать)
│ └── logger/ # Кастомный логгер
├── scripts/ # Скрипты (миграции, генерация)
├── configs/ # config.json, .env.example
├── migrations/ # SQL-миграции
├── test/ # Интеграционные тесты
├── Dockerfile
├── docker-compose.yml
└── README.md
Пример: слой конфигурации
// internal/config/config.go
package config
import (
"encoding/json"
"os"
"sync"
)
type Config struct {
Server struct {
Port int `json:"port"`
Host string `json:"host"`
} `json:"server"`
Database struct {
DSN string `json:"dsn"`
} `json:"database"`
}
var (
instance *Config
once sync.Once
)
func Load(path string) error {
var err error
once.Do(func() {
data, readErr := os.ReadFile(path)
if readErr != nil {
err = readErr
return
}
instance = &Config{}
err = json.Unmarshal(data, instance)
})
return err
}
func Get() *Config {
if instance == nil {
panic("конфигурация не загружена!")
}
return instance
}
Пример: слой базы данных
// internal/database/database.go
package database
import (
"database/sql"
"sync"
_ "github.com/lib/pq"
)
var (
db *sql.DB
once sync.Once
)
func Connect(dsn string) error {
var err error
once.Do(func() {
db, err = sql.Open("postgres", dsn)
if err != nil {
return
}
err = db.Ping()
})
return err
}
func GetDB() *sql.DB {
return db
}
Пример: HTTP-сервер (cmd/server/main.go)
// cmd/server/main.go
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"myproject/internal/api"
"myproject/internal/config"
"myproject/internal/database"
)
func main() {
if err := config.Load("configs/config.json"); err != nil {
log.Fatal(err)
}
if err := database.Connect(config.Get().Database.DSN); err != nil {
log.Fatal(err)
}
r := gin.Default()
api.RegisterRoutes(r)
slog.Info("Сервер запущен", "адрес", config.Get().Server.Port)
log.Fatal(http.ListenAndServe(":8080", r))
}
Лучшие практики
-
Именование пакетов — короткие, в нижнем регистре, без
utils,helpers,manager- Хорошо:
config,database,api,service - Плохо:
myutils,userManager
- Хорошо:
-
Экспортируйте осознанно — только то, что действительно нужно снаружи
-
Документируйте экспортируемые сущности
// Add складывает два числа и возвращает результат.
// Пример: Add(2, 3) => 5
func Add(a, b int) int {
return a + b
} -
Используйте
internal/для кода, который не должен утекать наружу -
Разделяйте слои:
api— HTTP, валидация запросовservice— бизнес-логикаdatabase— работа с БДmodels— структуры данных
Примерная структура проекта в итоге
myapi/
├── cmd
│ ├── main.go
├── internal
│ ├── config
│ │ ├── config.go
│ │ └── config.go.mod
│ ├── users
│ │ ├── users.go
│ │ └── users.go.mod
├── go.mod
├── go.sum
├── .env