1. Идиомы Go, стиль, ошибки и архитектура пакетов
Идиомы Go, стиль, ошибки и архитектура пакетов
Go ценится за простоту чтения, предсказуемость и единообразие. На уровне Middle от вас ожидают не просто «писать рабочий код», а писать его так, чтобы он:
Эта статья задаёт основу курса: идиоматический стиль, практики обработки ошибок и принципы организации кода.
Идиомы Go: как мыслит экосистема
Идиомы Go — это не «священные правила», а договорённости, которые делают код одинаково читаемым во всех командах.
Ясность важнее «умности»
Ключевой принцип: простое и прямолинейное решение предпочтительнее абстрактного, даже если абстрактное выглядит «красиво».
Практический вывод: прежде чем вводить паттерн, убедитесь, что он действительно снижает сложность, а не повышает.
Нулевое значение должно быть полезным
Хороший Go-тип должен быть работоспособен в нулевом состоянии (zero value) или хотя бы не приводить к панике.
Примеры из стандартной библиотеки:
sync.Mutex можно использовать без конструктораbytes.Buffer готов к работе без инициализацииВаши типы тоже стоит проектировать аналогично, когда это разумно.
Предпочитайте композицию наследованию
В Go нет классического наследования. Типовая идиома — композиция через встраивание и небольшие интерфейсы.
type X struct { Y }) — способ переиспользования без усложнения дерева типовПолезная ориентация: «принимай интерфейсы, возвращай структуры».
Интерфейсы должны быть маленькими
Чем меньше интерфейс, тем проще его реализовать и тестировать.
Хорошая практика:
Это снижает связность и облегчает мокирование.
Стиль и форматирование: единообразие как инструмент
gofmt и goimports
В Go форматирование — это не тема для обсуждений.
gofmt приводит код к стандартному видуgoimports делает то же самое и дополнительно управляет импортамиНа практике:
Ссылки:
Именование
Именование — один из главных источников читаемости.
data, info, manager, если они ничего не объясняютРаспространённые соглашения:
ID, HTTP, URL — аббревиатуры в CamelCase обычно пишутся как UserID, HTTPServerGetX, если это обычное поле/метод: user.Name() лучше, чем user.GetName()Официальные рекомендации:
Комментарии: зачем и где
Комментарий должен отвечать на вопрос «почему так?», а не «что делает код?».
Особое правило: экспортируемые сущности должны иметь комментарий в стиле GoDoc.
Пример:
Линтеры и статический анализ
На уровне Middle ожидается, что вы используете инструменты, которые предотвращают ошибки ещё до запуска.
go vet для типичных баговstaticcheck для более широкого спектра проблемgolangci-lint — удобно для CIСсылки:
Ошибки в Go: идиоматическая обработка
Ошибка — это значение
В Go ошибка — это обычное значение типа error. Это означает:
Классический шаблон:
Не игнорируйте ошибки
Игнорирование ошибок (_ = err) допустимо только если вы осознанно решили, что ошибка неважна, и это документировано/очевидно.
Если ошибка действительно неважна, лучше явно оформить это решением в коде (например, логирование с пояснением).
Контекст в ошибках: добавляйте смысл
Одна и та же ошибка нижнего уровня может происходить в разных местах. Оборачивайте её с контекстом:
Так вы получаете цепочку причин, полезную в логах и мониторинге.
Обёртка ошибок и сравнение: errors.Is и errors.As
С Go 1.13 рекомендуется:
%werrors.Iserrors.AsПример:
Ссылки:
Сентинельные ошибки: когда нужны и когда вредят
Сентинельная ошибка — это ошибка, которую сравнивают по идентичности (errors.Is или прямым сравнением), например io.EOF.
Используйте сентинелы, когда:
Избегайте, когда:
Типизированные ошибки (custom error types)
Когда нужно передать детали (например, поле, значение, код), используйте тип ошибки.
Выигрыш: вызывающий код может принимать решение на основе структурированных данных, а не парсинга строк.
Где логировать ошибку
Важно: ошибка должна быть залогирована ровно один раз на правильном уровне.
Практическое правило:
cmd, HTTP handler, worker) логирует и принимает решение (ретраи, статус-код)Если логировать на каждом уровне, вы получите шум и дубликаты.
Архитектура пакетов: как организовать проект
Цели хорошей архитектуры пакетов
Что такое пакет в Go
Пакет — это единица компиляции и публичного API.
Отсюда следует вывод: экспортируйте только то, что действительно нужно.
internal: как скрывать реализацию
Директория internal — механизм языка, который запрещает импорт пакетов извне родительского дерева.
Типовой сценарий:
Пример структуры:
cmd/myapp может импортировать myapp/internal/appmyapp/internal/appСсылка:
cmd: точки входа
cmd/ обычно содержит бинарники (один каталог — один main пакет).
cmd, иначе её сложнее переиспользовать и тестироватьpkg: публичные библиотеки (если они действительно нужны)
Если ваш репозиторий — приложение, pkg/ часто не нужен.
pkg/ оправдан, когда:
Популярный ориентир (не официальный стандарт, но часто используемый шаблон):
Разделение по слоям: домен, use cases, инфраструктура
На практике удобно разделять код по ответственности:
Ключевая мысль: зависимости должны идти внутрь, к более стабильным слоям.
!Диаграмма показывает, что внешние слои зависят от внутренних, а не наоборот
Избегайте циклических зависимостей
Циклы обычно появляются, когда пакеты смешивают ответственность.
Приёмы борьбы:
foo (API) и foo/internal (реализация)Не делайте «utils» без границ
Пакет utils или common почти всегда превращается в свалку.
Вместо этого:
clock, retry, httputil (если это реально утилиты вокруг HTTP)Практические рекомендации для Middle-разработчика
gofmt всегда, в идеале через pre-commit или IDEservice, use case)%w и errors.Is/As, а не сравнение строкinternal скрывал реализацию, а публичный API был минимальным