Go изначально проектировался не просто как язык, а как полная инструментальная среда. Утилиты форматирования, тестирования, профилирования и сборки идут в комплекте. Это глава о том, как эффективно использовать инструменты Go-экосистемы для промышленной разработки.
Предпосылки
Предполагается, что вы уже знакомы с основами Go из 01-basics и написанием тестов из 07-testing. Знание командной строки и базовые навыки работы с Git обязательны.
1. Форматирование кода
В Go нет споров о стиле кода. Форматирование стандартизировано на уровне инструментов.
go fmt
go fmt — встроенный форматтер, задающий единый стиль для всего Go-кода в мире:
# Форматирование одного файлаgo fmt main.go# Форматирование всего проектаgo fmt ./...# Посмотреть diff без изменения файловgofmt -d .
go fmt — обёртка над gofmt. Он применяет единственный набор правил: табуляция для отступов, определённое расположение скобок, пробелы вокруг операторов. Никаких настроек — это принципиальное решение.
gofumpt
gofumpt — строгая версия gofmt от Daniel Martian. Добавляет правила, которые стандартный форматтер не покрывает:
Включение gofumpt через gopls позволяет использовать строгое форматирование прямо в IDE без отдельного запуска утилиты.
🏠 Домашнее задание
Установите gofumpt и goimports. Отформатируйте свой проект обоими инструментами и сравните результат с go fmt.
Настройте VS Code для автоматического форматирования с группировкой импортов. Создайте файл с перемешанными импортами и убедитесь, что при сохранении они группируются правильно.
Создайте git pre-commit hook, который запускает gofumpt -d . и не позволяет коммитить неотформатированный код.
2. golangci-lint
golangci-lint — агрегатор линтеров для Go. Вместо запуска десятков отдельных утилит, он управляет ими через единый конфигурационный файл, кеширует результаты и выполняет анализ параллельно.
Установка и запуск
# Установка (рекомендуемый способ)curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.0# Или через go install (менее предпочтительно, так как компиляция из исходников)go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0# Проверка версииgolangci-lint --version# Запуск на всём проектеgolangci-lint run ./...# Запуск с автоисправлением (где возможно)golangci-lint run --fix ./...# Запуск с подробным выводомgolangci-lint run -v ./...# Запуск только определённых линтеровgolangci-lint run --enable errcheck,govet ./...
Конфигурация .golangci.yml
Полный пример конфигурации для продакшен-проекта:
# .golangci.ymlrun: # Таймаут на анализ (увеличьте для больших проектов) timeout: 5m # Пропустить тестовые файлы tests: false # Пропустить сгенерированные файлы skip-files: - ".*_gen\\.go$" - ".*_mock\\.go$"linters: # Отключить все линтеры по умолчанию disable-all: true # Включить только нужные enable: # Обязательные (уровень: критический) - errcheck # Необработанные ошибки - govet # Подозрительные конструкции - staticcheck # Комплексный статический анализ - unused # Неиспользуемый код - gosimple # Упрощение кода - ineffassign # Неэффективные присваивания - typecheck # Проверка типов # Рекомендуемые (уровень: важный) - revive # Замена golint, расширяемый линтер стиля - gocritic # Продвинутые проверки кода - misspell # Орфографические ошибки в комментариях и строках - errorlint # Правильное использование errors.Is/As - wrapcheck # Оборачивание ошибок из внешних пакетов # Безопасность - gosec # Проверки безопасности # Производительность - prealloc # Предварительное выделение слайсов - bodyclose # Закрытие resp.Body в HTTP-клиентах - noctx # Отсутствие context в HTTP-запросахlinters-settings: revive: rules: - name: exported severity: warning - name: unexported-return severity: warning - name: indent-error-flow severity: warning - name: error-naming severity: warning gocritic: enabled-tags: - diagnostic - style - performance disabled-checks: - hugeParam # Иногда передача больших структур по значению оправдана errcheck: # Проверять ошибки при type assertion check-type-assertions: true # Проверять ошибки из функций, присвоенных _ (blank identifier) check-blank: true govet: enable-all: true disable: - fieldalignment # Слишком педантично для большинства проектов staticcheck: checks: - "all" - "-SA1019" # Отключить предупреждения о deprecated (если нужно) gosec: excludes: - G104 # Необработанные ошибки (уже покрыто errcheck) misspell: locale: USissues: # Показывать все найденные проблемы (по умолчанию лимит 50) max-issues-per-linter: 0 max-same-issues: 0 # Исключения для тестовых файлов exclude-rules: - path: _test\.go linters: - errcheck # В тестах допустимо игнорировать ошибки - gosec # Тесты не нуждаются в проверках безопасности - wrapcheck # В тестах не обязательно оборачивать ошибки - gocritic # Исключить сгенерированный код - path: ".*_gen\\.go" linters: - revive - gocritic - errcheck # Директории для исключения exclude-dirs: - vendor - third_party - testdata - docs
Ключевые линтеры подробно
errcheck — необработанные ошибки
Самый важный линтер. Находит вызовы функций, возвращающих ошибку, которую вы проигнорировали:
// ПЛОХО: ошибка при записи в файл проигнорированаf, _ := os.Create("data.txt")f.Write([]byte("данные")) // errcheck: ошибка не проверенаf.Close() // errcheck: ошибка не проверена// ХОРОШО: все ошибки обработаныf, err := os.Create("data.txt")if err != nil { return fmt.Errorf("создание файла: %w", err)}defer func() { if closeErr := f.Close(); closeErr != nil { log.Printf("ошибка закрытия файла: %v", closeErr) }}()if _, err := f.Write([]byte("данные")); err != nil { return fmt.Errorf("запись в файл: %w", err)}
govet — подозрительные конструкции
Встроенный в Go анализатор. Находит ошибки, которые компилятор пропускает:
// ПЛОХО: неверный формат в printf (govet: Printf format %d has arg of wrong type)fmt.Printf("имя: %d\n", "Иван")// ПЛОХО: копирование мьютекса (govet: copylocks)type Cache struct { mu sync.Mutex data map[string]string}func process(c Cache) { // govet: передача Cache по значению копирует мьютекс c.mu.Lock() defer c.mu.Unlock()}// ХОРОШО: передача по указателюfunc process(c *Cache) { c.mu.Lock() defer c.mu.Unlock()}
staticcheck — комплексный анализ
Самый мощный линтер. Включает сотни проверок:
// SA1012: передача nil context (staticcheck)req, _ := http.NewRequest("GET", url, nil) // нет контекста// SA4006: значение переменной никогда не используется после присваиванияx := computeValue()x = 42 // staticcheck: предыдущее значение x никогда не использовалось// S1002: упрощение bool-сравнения (gosimple)if isReady == true { // можно упростить до: if isReady {}// S1039: ненужный вызов fmt.Sprintffmt.Sprintf("%s", someString) // можно просто использовать someString
revive — замена golint
Расширяемый линтер стиля с настраиваемыми правилами:
// exported: экспортируемая функция без документацииfunc ProcessOrder(o *Order) error { // revive: exported function ProcessOrder should have comment // ...}// indent-error-flow: ранний возврат предпочтительнееfunc validate(s string) error { // ПЛОХО if len(s) > 0 { // длинная логика return nil } else { return errors.New("пустая строка") } // ХОРОШО (ранний возврат) if len(s) == 0 { return errors.New("пустая строка") } // основная логика return nil}
gocritic — продвинутые проверки
Линтер с обширным набором проверок, разделённых на категории:
// diagnostic: appendAssign -- подозрительное присваивание при append// (если забыли присвоить результат обратно)data := []int{1, 2, 3}append(data, 4) // gocritic: результат append не присвоен// style: ifElseChain -- цепочка if-else может быть switchif x == 1 { // ...} else if x == 2 { // ...} else if x == 3 { // ...}// gocritic: рекомендуется switch// performance: rangeValCopy -- копирование большой структуры в rangetype Big struct { Data [1024]byte}items := []Big{{}, {}}for _, item := range items { // gocritic: копирует 1024 байта на каждой итерации _ = item}// Исправление: for i := range items { _ = items[i] }
gosec — безопасность
Находит потенциальные уязвимости:
// G101: захардкоженные учётные данныеconst password = "super_secret_123" // gosec: hardcoded credentials// G201: SQL-инъекцияquery := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput) // gosec!db.Query(query)// ХОРОШО: параметризованный запросdb.Query("SELECT * FROM users WHERE name = $1", userInput)// G304: чтение файла из пользовательского вводаfilePath := r.URL.Query().Get("file")data, _ := os.ReadFile(filePath) // gosec: path traversal// G401: использование слабого хешированияh := md5.New() // gosec: use of weak cryptographic primitive
Не запускайте golangci-lint с флагом --enable-all. Многие линтеры конфликтуют между собой. Лучше включить конкретный набор и постепенно расширять его.
🏠 Домашнее задание
Установите golangci-lint и создайте .golangci.yml для своего проекта. Включите минимум 10 линтеров из описанных выше.
Запустите линтер на своём проекте и исправьте все найденные проблемы. Запишите, какие ошибки встречались чаще всего.
Добавьте golangci-lint в GitHub Actions. Создайте PR с намеренной ошибкой (необработанный error) и убедитесь, что CI не проходит.
Настройте exclude-rules для тестовых файлов и сгенерированного кода.
3. Профилирование: pprof
Go имеет встроенные инструменты профилирования через пакет runtime/pprof и его HTTP-обёртку net/http/pprof. Это ключевой инструмент для поиска узких мест в продакшене.
Подключение net/http/pprof
package mainimport ( "log" "net/http" _ "net/http/pprof" // Регистрирует обработчики на DefaultServeMux)func main() { // Запуск pprof-сервера на отдельном порту (не на основном!) go func() { // ВАЖНО: pprof-эндпоинт не должен быть доступен извне log.Println("pprof сервер запущен на :6060") if err := http.ListenAndServe("localhost:6060", nil); err != nil { log.Printf("pprof сервер: %v", err) } }() // Основной сервер приложения mux := http.NewServeMux() mux.HandleFunc("/api/v1/users", handleUsers) log.Fatal(http.ListenAndServe(":8080", mux))}
Безопасность
Никогда не открывайте pprof на публичном адресе (0.0.0.0). Он позволяет читать данные из памяти процесса. Используйте localhost:6060 и доступ через SSH-туннель или kubectl port-forward.
После запуска сервера доступны эндпоинты:
http://localhost:6060/debug/pprof/ — индекс всех профилей
http://localhost:6060/debug/pprof/heap — профиль памяти
http://localhost:6060/debug/pprof/goroutine — все горутины
http://localhost:6060/debug/pprof/mutex — конкуренция за мьютексы
http://localhost:6060/debug/pprof/trace?seconds=5 — трассировка выполнения
CPU-профилирование
CPU-профиль показывает, на какие функции тратится процессорное время:
# Собрать CPU-профиль за 30 секундgo tool pprof http://localhost:6060/debug/pprof/profile?seconds=30# Откроется интерактивная консоль pprof# Основные команды:(pprof) top 20 # Топ-20 функций по потреблению CPU(pprof) top -cum # Топ по кумулятивному времени (включая вызовы)(pprof) list handleUsers # Показать код функции с аннотациями времени(pprof) web # Открыть граф вызовов в браузере (нужен graphviz)(pprof) png > cpu.png # Сохранить граф в PNG
flat — время, проведённое непосредственно в этой функции
cum — кумулятивное время (функция + все вызванные ею функции)
Профилирование памяти
# Собрать heap-профильgo tool pprof http://localhost:6060/debug/pprof/heap(pprof) top # Кто больше всего выделяет памяти(pprof) top -inuse_space # По текущему использованию (не по аллокациям)(pprof) list processData # Код с аннотациями по памяти
Два режима анализа памяти:
inuse_space / inuse_objects — что сейчас в памяти (ищем утечки)
alloc_space / alloc_objects — что было аллоцировано суммарно (ищем нагрузку на GC)
Горутинный профиль показывает все текущие горутины и их стеки — незаменимый инструмент для поиска утечек:
# Посмотреть все горутиныgo tool pprof http://localhost:6060/debug/pprof/goroutine(pprof) top # Группировка по стекам вызовов(pprof) traces # Полные стектрейсы всех горутин# Или в текстовом формате через curlcurl http://localhost:6060/debug/pprof/goroutine?debug=2
Обнаружение утечки горутин
Если число горутин растёт со временем — у вас утечка. Сравните профили:
# Сохранить профиль сейчасcurl -o goroutines_before.prof http://localhost:6060/debug/pprof/goroutine# Подождать и сохранить сноваcurl -o goroutines_after.prof http://localhost:6060/debug/pprof/goroutine# Сравнитьgo tool pprof -base goroutines_before.prof goroutines_after.prof
Block-профилирование
Показывает, где горутины блокируются на синхронизации (каналы, мьютексы, select):
// Нужно явно включить в кодеruntime.SetBlockProfileRate(1) // 1 = записывать каждое событие блокировки
go tool pprof http://localhost:6060/debug/pprof/block(pprof) top # Где чаще всего блокируются горутины
Визуализация: веб-интерфейс и flame graphs
# Открыть интерактивный веб-интерфейсgo tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30# Откроется в браузере с несколькими представлениями:# - Graph: граф вызовов# - Flame Graph: стековая диаграмма (flame graph)# - Top: таблица топ функций# - Source: исходный код с аннотациями# - Peek: контекст вызовов функции
Flame graph — самый наглядный инструмент. Ширина прямоугольника пропорциональна времени. Ищите широкие блоки — это узкие места.
Практический пример: поиск узкого места
// Медленный обработчик -- попробуем найти, где тормозитfunc handleReport(w http.ResponseWriter, r *http.Request) { // Получаем данные из БД rows, err := db.QueryContext(r.Context(), "SELECT id, name, data FROM reports WHERE status = 'active'") if err != nil { http.Error(w, err.Error(), 500) return } defer rows.Close() var reports []Report for rows.Next() { var rpt Report if err := rows.Scan(&rpt.ID, &rpt.Name, &rpt.Data); err != nil { http.Error(w, err.Error(), 500) return } // Тяжёлая обработка каждого отчёта rpt.Summary = generateSummary(rpt.Data) // <-- узкое место reports = append(reports, rpt) } // Сериализация в JSON data, err := json.Marshal(reports) if err != nil { http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "application/json") w.Write(data)}
Профилирование покажет, что generateSummary занимает 70% CPU-времени. Дальше смотрим list generateSummary и видим конкретную строку.
🏠 Домашнее задание
Подключите net/http/pprof к своему проекту. Нагрузите приложение (например, с помощью hey или ab) и соберите CPU-профиль. Найдите топ-5 горячих функций.
Соберите heap-профиль и определите, какие объекты занимают больше всего памяти.
Создайте намеренную утечку горутин (горутина, которая ждёт на канале, куда никто не пишет) и обнаружьте её через goroutine-профиль.
Визуализируйте CPU-профиль с помощью flame graph (go tool pprof -http=:8081).
4. go tool trace
Если pprof показывает, сколько времени потрачено в каждой функции, то go tool trace показывает, когда и как выполнялись горутины во времени.
# Вариант 2: Сбор через HTTP (если подключён net/http/pprof)curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5# Вариант 3: Сбор при запуске тестовgo test -trace=trace.out ./...
Визуализация
# Открыть трассировку в браузереgo tool trace trace.out
В браузере открывается интерфейс со следующими представлениями:
Goroutine analysis — группировка горутин по функции создания, время жизни каждой горутины
Network blocking profile — блокировки на сетевых операциях
Synchronization blocking profile — блокировки на синхронизации (мьютексы, каналы)
Syscall blocking profile — блокировки на системных вызовах
События GC (сборка мусора): STW-паузы, маркировка, очистка
Сетевые вызовы и системные вызовы
Создание новых горутин
Когда использовать trace вместо pprof
Задержки (latency) сложно объяснить — нагрузка на CPU низкая, но запросы медленные
Проблемы с GC — частые или длинные паузы
Планировщик горутин — горутины ждут в очереди на выполнение
Конкуренция за ресурсы — горутины блокируют друг друга
🏠 Домашнее задание
Соберите трассировку своего HTTP-сервера под нагрузкой. Откройте View trace и найдите самую длинную горутину.
Создайте программу с интенсивной аллокацией памяти и визуализируйте GC-паузы через trace.
Сравните результаты pprof и trace для одного и того же сценария. Объясните, какую информацию даёт каждый инструмент.
5. Отладка: delve (dlv)
delve — отладчик, разработанный специально для Go. Он понимает горутины, каналы, интерфейсы и другие конструкции языка.
Установка
go install github.com/go-delve/delve/cmd/dlv@latest# Проверкаdlv version
Базовая отладка
# Запуск программы под отладчикомdlv debug ./cmd/server# Запуск с аргументамиdlv debug ./cmd/server -- --port 8080# Запуск тестов под отладчикомdlv test ./internal/service/...
Управление брейкпоинтами
# Установить брейкпоинт по имени функции(dlv) break main.main(dlv) break github.com/myapp/internal/service.(*UserService).Create# Установить по файлу и строке(dlv) break ./internal/handler/user.go:42# Условный брейкпоинт -- остановиться только если условие истинно(dlv) break ./internal/handler/user.go:42(dlv) condition 1 userID == 15# Список брейкпоинтов(dlv) breakpoints# Удалить брейкпоинт(dlv) clear 1# Удалить все брейкпоинты(dlv) clearall
Навигация по коду
# Продолжить до следующего брейкпоинта(dlv) continue # или: c# Следующая строка (не заходя в вызываемые функции)(dlv) next # или: n# Шаг внутрь функции(dlv) step # или: s# Выйти из текущей функции(dlv) stepout# Выполнить до указанной строки(dlv) break ./handler.go:50(dlv) continue
Инспекция состояния
# Вывести значение переменной(dlv) print userID(dlv) print user.Name(dlv) print len(users)(dlv) print users[0]# Все локальные переменные(dlv) locals# Аргументы текущей функции(dlv) args# Текущий стек вызовов(dlv) stack# Переключиться на другой фрейм стека(dlv) frame 2# Список всех горутин(dlv) goroutines# Переключиться на другую горутину(dlv) goroutine 15# Стек конкретной горутины(dlv) goroutine 15 stack
Подключение к запущенному процессу
# Найти PID процессаps aux | grep myserver# Подключитьсяdlv attach 12345# После отладки -- отсоединиться (процесс продолжит работу)(dlv) detach
Удалённая отладка в контейнерах
# Dockerfile для отладкиFROM golang:1.23RUN go install github.com/go-delve/delve/cmd/dlv@latestWORKDIR /appCOPY . .RUN go build -gcflags="all=-N -l" -o server ./cmd/server# Запуск через delve в headless-режимеCMD ["dlv", "exec", "./server", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient"]
# Запуск контейнера с открытым портом отладчикаdocker run -p 2345:2345 -p 8080:8080 myapp-debug# Подключение с хостаdlv connect localhost:2345
Флаги компиляции для отладки
Флаг -gcflags="all=-N -l" отключает оптимизации компилятора (-N) и инлайнинг (-l). Без этих флагов отладчик может показывать некорректные значения переменных, потому что компилятор оптимизирует их.
После настройки: ставим брейкпоинты кликом по полю слева от номера строки, запускаем через F5, инспектируем переменные в панели Variables.
🏠 Домашнее задание
Установите delve. Запустите свой проект под отладчиком, установите брейкпоинт в обработчике HTTP-запроса и отправьте запрос. Исследуйте стек вызовов и значения переменных.
Создайте условный брейкпоинт, который срабатывает только для определённого ID пользователя.
Настройте удалённую отладку в Docker-контейнере и подключитесь к ней из VS Code.
Используйте команду goroutines для исследования всех активных горутин в работающем сервере.
6. Генерация кода: go generate
go generate запускает команды, указанные в специальных комментариях в Go-файлах. Это механизм для кодогенерации — создания Go-кода на основе других источников.
Директива //go:generate
// Синтаксис: //go:generate команда аргументы// ВАЖНО: между // и go:generate нет пробела!//go:generate stringer -type=Status//go:generate mockgen -source=repository.go -destination=mock_repository.go//go:generate protoc --go_out=. --go-grpc_out=. proto/api.proto
# Запуск генерации для всего проектаgo generate ./...# Запуск для конкретного пакетаgo generate ./internal/model/...# Показать команды без выполнения (dry run)go generate -n ./...# С выводом выполняемых командgo generate -v ./...
stringer: генерация String() для перечислений
package model//go:generate stringer -type=OrderStatus// OrderStatus представляет статус заказаtype OrderStatus intconst ( OrderStatusPending OrderStatus = iota // В ожидании OrderStatusConfirmed // Подтверждён OrderStatusProcessing // В обработке OrderStatusShipped // Отправлен OrderStatusDelivered // Доставлен OrderStatusCancelled // Отменён)
# Установка stringergo install golang.org/x/tools/cmd/stringer@latest# Генерацияgo generate ./internal/model/...
Создаётся файл orderstatus_string.go:
// Code generated by "stringer -type=OrderStatus"; DO NOT EDIT.package modelfunc (i OrderStatus) String() string { switch i { case OrderStatusPending: return "OrderStatusPending" case OrderStatusConfirmed: return "OrderStatusConfirmed" // ... и так далее } return fmt.Sprintf("OrderStatus(%d)", i)}
mockgen: генерация моков из интерфейсов
package service//go:generate mockgen -source=user_service.go -destination=mock_user_service.go -package=service// UserRepository определяет операции с хранилищем пользователейtype UserRepository interface { GetByID(ctx context.Context, id int64) (*User, error) Create(ctx context.Context, user *User) error Update(ctx context.Context, user *User) error Delete(ctx context.Context, id int64) error List(ctx context.Context, filter UserFilter) ([]User, error)}
# Установка mockgengo install go.uber.org/mock/mockgen@latest# Два режима генерации:# 1. Source mode -- из файла с интерфейсомmockgen -source=repository.go -destination=mock_repository.go -package=service# 2. Reflect mode -- по имени пакета и интерфейсаmockgen -destination=mock_repository.go -package=service \ github.com/myapp/internal/service UserRepository
CI должен уметь запускать тесты без codegen-зависимостей
Protobuf / gRPC
Да
Не все среды имеют protoc установленным
sqlc
Да
Прозрачность — виден код, который реально исполняется
stringer
Да
Простая зависимость, но файл маленький
OpenAPI-клиенты
Да
Стабильность сборки
Конвенция: пометка сгенерированных файлов
Все Go-генераторы добавляют маркер в первую строку:
// Code generated by <tool>; DO NOT EDIT.
Этот маркер:
Линтеры (golangci-lint) автоматически пропускают такие файлы
IDE показывает предупреждение при попытке редактирования
go generate знает, что файл можно перезаписать
🏠 Домашнее задание
Создайте enum Role (Admin, Manager, User, Guest) и сгенерируйте для него String() с помощью stringer.
Определите интерфейс OrderRepository и сгенерируйте мок с помощью mockgen. Напишите тест с использованием сгенерированного мока.
Напишите SQL-запросы для CRUD-операций с таблицей products и сгенерируйте Go-код через sqlc.
Создайте Makefile-таргет generate, который запускает go generate ./....
7. Swagger/OpenAPI: swaggo
swaggo — инструмент для генерации Swagger/OpenAPI-документации из аннотаций в Go-коде. Он позволяет поддерживать документацию API рядом с реализацией.
Установка
go install github.com/swaggo/swag/cmd/swag@latest
Аннотации главного файла
// @title API Интернет-магазина// @version 1.0// @description REST API для управления товарами и заказами// @termsOfService http://swagger.io/terms/// @contact.name Команда разработки// @contact.email dev@mycompany.com// @license.name Apache 2.0// @license.url http://www.apache.org/licenses/LICENSE-2.0.html// @host localhost:8080// @BasePath /api/v1// @securityDefinitions.apikey BearerAuth// @in header// @name Authorization// @description Введите токен в формате: Bearer <token>func main() { // ...}
Аннотации обработчиков
// CreateOrder создаёт новый заказ// @Summary Создание заказа// @Description Создаёт новый заказ для авторизованного пользователя// @Tags orders// @Accept json// @Produce json// @Param request body CreateOrderRequest true "Данные заказа"// @Success 201 {object} OrderResponse "Заказ создан"// @Failure 400 {object} ErrorResponse "Невалидные данные"// @Failure 401 {object} ErrorResponse "Не авторизован"// @Failure 500 {object} ErrorResponse "Внутренняя ошибка"// @Security BearerAuth// @Router /orders [post]func (h *OrderHandler) CreateOrder(c *gin.Context) { var req CreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Message: err.Error()}) return } // ...}// GetOrder возвращает заказ по ID// @Summary Получение заказа// @Description Возвращает полную информацию о заказе// @Tags orders// @Produce json// @Param id path int true "ID заказа" minimum(1)// @Success 200 {object} OrderResponse// @Failure 404 {object} ErrorResponse "Заказ не найден"// @Security BearerAuth// @Router /orders/{id} [get]func (h *OrderHandler) GetOrder(c *gin.Context) { // ...}// ListOrders возвращает список заказов с пагинацией// @Summary Список заказов// @Description Возвращает список заказов текущего пользователя// @Tags orders// @Produce json// @Param page query int false "Номер страницы" default(1) minimum(1)// @Param per_page query int false "Элементов на странице" default(20) minimum(1) maximum(100)// @Param status query string false "Фильтр по статусу" Enums(pending, confirmed, shipped, delivered)// @Success 200 {object} ListOrdersResponse// @Security BearerAuth// @Router /orders [get]func (h *OrderHandler) ListOrders(c *gin.Context) { // ...}
Модели для Swagger
// CreateOrderRequest -- запрос на создание заказаtype CreateOrderRequest struct { // Список товаров в заказе Items []OrderItem `json:"items" binding:"required,min=1" example:"[]"` // Адрес доставки Address string `json:"address" binding:"required" example:"ул. Пушкина, д. 10"` // Комментарий к заказу Comment string `json:"comment,omitempty" example:"Позвонить за час до доставки"`} // @name CreateOrderRequest// OrderResponse -- ответ с данными заказаtype OrderResponse struct { // Уникальный идентификатор ID int64 `json:"id" example:"42"` // Статус заказа Status string `json:"status" example:"pending"` // Итоговая сумма Total float64 `json:"total" example:"1599.99"` // Дата создания CreatedAt time.Time `json:"created_at" example:"2025-01-15T10:30:00Z"`} // @name OrderResponse// ErrorResponse -- стандартный ответ об ошибкеtype ErrorResponse struct { // Описание ошибки Message string `json:"message" example:"заказ не найден"` // Код ошибки для программной обработки Code string `json:"code,omitempty" example:"ORDER_NOT_FOUND"`} // @name ErrorResponse
# Taskfile.ymlversion: "3"vars: BINARY_NAME: myapp BUILD_DIR: ./bintasks: build: desc: "Сборка приложения" cmds: - go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} ./cmd/server sources: - ./**/*.go generates: - "{{.BUILD_DIR}}/{{.BINARY_NAME}}" run: desc: "Запуск с hot-reload" cmds: - air test: desc: "Запуск тестов" cmds: - go test -race -count=1 ./... lint: desc: "Линтинг" cmds: - golangci-lint run ./... check: desc: "Полная проверка (lint + test)" deps: [lint, test] docker:up: desc: "Запуск Docker-инфраструктуры" cmds: - docker compose up -d docker:down: desc: "Остановка Docker-инфраструктуры" cmds: - docker compose down
# Использованиеtask buildtask testtask check # lint и test параллельноtask docker:up
Task vs Make
Task поддерживает кросс-платформенность (Windows), YAML-синтаксис, зависимости задач с параллельным выполнением, инкрементальные сборки через sources/generates. Make остаётся стандартом в Go-сообществе, но Task набирает популярность.
🏠 Домашнее задание
Добавьте samber/lo в проект и перепишите циклы обработки коллекций с использованием Map, Filter, GroupBy.
Создайте CLI-приложение на cobra с командами serve, migrate up, migrate down, seed. Добавьте глобальный флаг --config.
Настройте viper для чтения конфигурации из файла, переменных окружения и флагов одновременно. Проверьте приоритет: флаги > env > файл > defaults.
Настройте Air для горячей перезагрузки. Измените обработчик и убедитесь, что сервер перезапустился автоматически.
10. Makefile
Makefile — центральная точка входа для всех операций с проектом. Хорошо написанный Makefile документирует, как собирать, тестировать и запускать проект.
Полный Makefile для Go-проекта
# Makefile для Go-проекта.PHONY: build run test test-integration test-coverage lint fmt check \ migrate-up migrate-down migrate-create \ docker-build docker-up docker-down \ generate swagger deps tools help# ПеременныеAPP_NAME := myappBUILD_DIR := ./binMAIN_PATH := ./cmd/serverMIGRATION_DIR := ./migrationsDOCKER_COMPOSE := docker compose# Версия из gitVERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)"# Настройки линтераGOLANGCI_LINT_VERSION := v1.62.0# ==================== Сборка ====================## build: Сборка бинарникаbuild: @echo "Сборка $(APP_NAME)..." go build $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) @echo "Бинарник: $(BUILD_DIR)/$(APP_NAME)"## run: Запуск с hot-reload через Airrun: @air## run-binary: Запуск скомпилированного бинарникаrun-binary: build $(BUILD_DIR)/$(APP_NAME) --config config.local.yaml# ==================== Тестирование ====================## test: Запуск unit-тестовtest: go test -race -count=1 -short ./...## test-integration: Запуск интеграционных тестов (требует запущенной БД)test-integration: go test -race -count=1 -run Integration ./...## test-coverage: Тесты с отчётом покрытияtest-coverage: go test -race -coverprofile=coverage.out -covermode=atomic ./... go tool cover -func=coverage.out @echo "" @echo "HTML-отчёт: go tool cover -html=coverage.out -o coverage.html"## test-coverage-html: Тесты с HTML-отчётом покрытияtest-coverage-html: test-coverage go tool cover -html=coverage.out -o coverage.html @echo "Отчёт сохранён в coverage.html"# ==================== Качество кода ====================## lint: Запуск golangci-lintlint: golangci-lint run ./...## lint-fix: Запуск golangci-lint с автоисправлениемlint-fix: golangci-lint run --fix ./...## fmt: Форматирование кодаfmt: gofumpt -w . goimports -w -local github.com/mycompany/$(APP_NAME) .## check: Полная проверка (fmt + lint + test)check: fmt lint test @echo "Все проверки пройдены!"## vet: Запуск go vetvet: go vet ./...# ==================== Миграции ====================## migrate-up: Применить все миграцииmigrate-up: goose -dir $(MIGRATION_DIR) postgres "$(DATABASE_URL)" up## migrate-down: Откатить последнюю миграциюmigrate-down: goose -dir $(MIGRATION_DIR) postgres "$(DATABASE_URL)" down## migrate-status: Статус миграцийmigrate-status: goose -dir $(MIGRATION_DIR) postgres "$(DATABASE_URL)" status## migrate-create: Создать новую миграцию (usage: make migrate-create NAME=create_users)migrate-create: @if [ -z "$(NAME)" ]; then \ echo "Ошибка: укажите NAME=имя_миграции"; \ exit 1; \ fi goose -dir $(MIGRATION_DIR) create $(NAME) sql# ==================== Docker ====================## docker-build: Сборка Docker-образаdocker-build: docker build -t $(APP_NAME):$(VERSION) .## docker-up: Запуск инфраструктуры (postgres, redis и т.д.)docker-up: $(DOCKER_COMPOSE) up -d## docker-down: Остановка инфраструктурыdocker-down: $(DOCKER_COMPOSE) down## docker-logs: Логи контейнеровdocker-logs: $(DOCKER_COMPOSE) logs -f# ==================== Кодогенерация ====================## generate: Запуск go generategenerate: go generate ./...## swagger: Генерация Swagger-документацииswagger: swag init -g $(MAIN_PATH)/main.go -o docs --parseDependency --parseInternal swag fmt## proto: Генерация из protobufproto: protoc --go_out=. --go-grpc_out=. proto/**/*.proto## sqlc: Генерация из SQL-запросовsqlc: sqlc generate# ==================== Зависимости ====================## deps: Скачать и привести в порядок зависимостиdeps: go mod download go mod tidy go mod verify## tools: Установка необходимых инструментовtools: go install mvdan.cc/gofumpt@latest go install golang.org/x/tools/cmd/goimports@latest go install github.com/air-verse/air@latest go install github.com/swaggo/swag/cmd/swag@latest go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install go.uber.org/mock/mockgen@latest go install golang.org/x/tools/cmd/stringer@latest go install github.com/pressly/goose/v3/cmd/goose@latest curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin $(GOLANGCI_LINT_VERSION) @echo "Все инструменты установлены!"# ==================== Помощь ====================## help: Показать список доступных командhelp: @echo "Доступные команды:" @echo "" @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' | column -t -s ':'
Объяснение ключевых таргетов
Таргет
Назначение
build
Компилирует бинарник с вшитой версией из git tag
run
Запускает Air для hot-reload при разработке
test
Unit-тесты с race detector и без кеширования
test-integration
Интеграционные тесты (отдельно, так как требуют инфраструктуру)
test-coverage
Тесты с отчётом покрытия кода
lint
Запускает golangci-lint с конфигурацией из .golangci.yml
Последовательно: fmt, lint, test — полная проверка перед коммитом
migrate-*
Управление миграциями через goose
docker-*
Управление Docker-инфраструктурой
generate
Вся кодогенерация: моки, stringer, sqlc
swagger
Генерация Swagger-документации из аннотаций
deps
Скачивание и валидация зависимостей
tools
Установка всех необходимых CLI-инструментов
help
Автоматически генерирует список команд из комментариев ##
.PHONY
Все таргеты объявлены как .PHONY, потому что они не создают файлы с такими именами. Без этого Make мог бы пропустить выполнение, если случайно существует файл test или build.
🏠 Домашнее задание
Создайте Makefile для своего проекта, адаптировав пример выше. Добавьте вшивание версии через LDFLAGS.
Реализуйте таргет help, который парсит комментарии ## и выводит красивый список команд.
Добавьте таргет check и убедитесь, что он проходит локально перед каждым коммитом.
Настройте make tools для установки всех инструментов, необходимых новому разработчику.
11. Воркфлоу разработки
Полный цикл разработки Go-проекта — от клонирования до деплоя. Этот процесс стандартизирован для большинства Go-команд.
Начальная настройка
# 1. Клонирование репозиторияgit clone git@github.com:mycompany/myapp.gitcd myapp# 2. Установка инструментов (один раз)make tools# 3. Запуск инфраструктуры (PostgreSQL, Redis, Kafka и т.д.)make docker-up# 4. Применение миграцийmake migrate-up# 5. Копирование конфигурации для локальной разработкиcp config.example.yaml config.local.yaml# Отредактировать config.local.yaml при необходимости# 6. Запуск в режиме разработки (с hot-reload)make run
Цикл разработки
# 1. Создание веткиgit checkout -b feature/add-payment-service# 2. Написание кода# ... редактирование файлов ...# 3. Если добавили/изменили интерфейсы -- обновить мокиmake generate# 4. Если изменили API -- обновить Swaggermake swagger# 5. Проверка перед коммитомmake check # fmt + lint + test# 6. Коммит и пушgit add -Agit commit -m "feat: add payment service integration"git push -u origin feature/add-payment-service# 7. Создание Pull Requestgh pr create --fill
Pre-commit hooks
Автоматическая проверка кода перед каждым коммитом:
#!/bin/bash# .githooks/pre-commitset -eecho "=== Pre-commit проверки ==="# Форматированиеecho "Проверка форматирования..."UNFORMATTED=$(gofumpt -l . 2>&1)if [ -n "$UNFORMATTED" ]; then echo "Следующие файлы не отформатированы:" echo "$UNFORMATTED" echo "Запустите: make fmt" exit 1fi# Линтинг (только изменённые файлы для скорости)echo "Линтинг..."golangci-lint run --new-from-rev=HEAD ./...# Тестыecho "Тесты..."go test -race -short -count=1 ./...echo "=== Все проверки пройдены ==="
# Установка хуковgit config core.hooksPath .githookschmod +x .githooks/pre-commit
Трассировка: go tool trace — визуализация выполнения горутин
Отладка: delve — полноценный отладчик с поддержкой горутин
Кодогенерация: go generate — stringer, mockgen, sqlc, protoc
API-документация: swaggo — Swagger из аннотаций в коде
DI: ручной подход в main(), wire для больших проектов, fx для микросервисов
Библиотеки: lo, cobra, viper, Air, Task
Автоматизация: Makefile — единая точка входа для всех команд
Rob Pike
“The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not able to understand a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.”
— Инструменты Go отражают эту философию: они просты, предсказуемы и работают из коробки.