Go-стиль¶
Правила, по которым пишется Go-код в проекте. Это дополнение к общепринятым Go-гайдам, а не их замена. Обязательно прочитай публичные гайды:
Если что-то не описано здесь — действует общепринятая практика из этих гайдов.
Именование¶
Файлы и пакеты¶
- Имя пакета совпадает с именем папки: папка
service/→package service. - Имя пакета — одно слово, нижний регистр, без подчёркиваний и CamelCase:
handler,postgres,watermill, но НЕhttp_handler,userService. - Файлы —
snake_case.go:review_service.go,http_helpers.go. - Тесты рядом с кодом:
review_service.go→review_service_test.go.
Идентификаторы¶
- Exported (
PascalCase):UserRepo,NewAuthService,ErrNotFound. - Unexported (
camelCase):hashPassword,claimsFromCtx. - Константы — такие же:
MaxRetries,defaultTimeout. НеMAX_RETRIES. - Сокращения сохраняют регистр целиком:
userID,HTTPClient,URLParser, но НЕuserId,HttpClient,UrlParser. - Интерфейсы — по роли, не по реализации:
Publisher,TokenChecker,ReviewRepository. Одно-методные интерфейсы — суффикс-er(Reader,Marshaler).
JSON и SQL — snake_case¶
JSON-теги всегда snake_case:
type CreateReviewRequest struct {
PlaceID int64 `json:"place_id" validate:"required,gt=0"`
Rating int16 `json:"rating" validate:"required,min=1,max=5"`
Text string `json:"text,omitempty" validate:"max=2000"`
}
Колонки таблиц — тоже snake_case: created_at, place_id, is_active.
Обработка ошибок¶
Полные правила по sentinel-значениям, wrapping через %w, errors.Is /
errors.As, кастомным типам и маппингу на HTTP — в
error-handling.md. Здесь — только stylistic-
минимум, без которого go-style неполный.
- Всегда оборачивай через
%w:fmt.Errorf("load user %d: %w", id, err). Никогда%s/%vдля ошибки, которую надо будет проверить. - Префикс в
fmt.Errorf— императив, нижний регистр:"get user", а не"Error getting user". - Sentinel-ошибки объявляй пакетными переменными (
var ErrXxx = errors.New(...)) вerrors.goтого слоя, где они рождаются. - Сравнение — только
errors.Is/errors.As. Никакихerr == ErrXи никаких type-assertion'ов (err.(*MyErr)).
Никаких голых panic в прод-коде¶
panicдопустим только вinit()или вmain.goпри фатальной ошибке старта (например, не загрузился обязательный конфиг).- В handler'ах, service'ах, repository — возвращай
error. Поймай panic из third-party черезmiddleware.Recoverer(chi), а не заворачивай вrecoverвручную. - В тестах
t.Fatal— это неpanic, это нормально.
Context¶
context.Context — первый параметр¶
// хорошо
func (r *UserRepo) Get(ctx context.Context, id int64) (*domain.User, error)
// плохо — ctx не первый
func (r *UserRepo) Get(id int64, ctx context.Context) (*domain.User, error)
// плохо — нет ctx вообще для I/O операции
func (r *UserRepo) Get(id int64) (*domain.User, error)
context.Context НЕ храни в struct¶
// хорошо
type Worker struct { /* ... */ }
func (w *Worker) Run(ctx context.Context) error { ... }
// плохо
type Worker struct {
ctx context.Context // никогда так не делай
}
Исключение: если тип — «short-lived per-request» (например, SSE-subscriber), то контекст в нём ОК, но такие типы живут не дольше одного запроса.
context.Value — только для request-scoped¶
Разрешено: - user ID из JWT после middleware, - request ID, correlation ID, - locale / trace headers.
Запрещено: - передавать бизнес-параметры (filter, page size) через context, - передавать зависимости (репозиторий, клиент) через context.
Ключи контекста — unexported-типы:
Это предотвращает коллизии между пакетами.
Структура функций¶
Короткие функции¶
Если функция > 50 строк или > 3 уровней вложенности — декомпозируй. Обычно это сигнал, что внутри прячется два-три шага, каждый из которых заслуживает отдельной функции.
Ранний выход¶
// хорошо
if err != nil {
return fmt.Errorf("load: %w", err)
}
if user.Banned {
return ErrUserBanned
}
// happy-path продолжается без вложений
Не пиши глубокие else-лестницы.
Именованные return-значения — умеренно¶
Используй именованные returns, когда хочешь задокументировать семантику:
Не используй их ради «naked return» в середине длинной функции. Это путает.
Интерфейсы¶
Принимай интерфейс, возвращай конкретный тип¶
// service зависит от интерфейса
type UserRepo interface {
Get(ctx context.Context, id int64) (*domain.User, error)
}
func NewAuthService(repo UserRepo) *AuthService { ... }
// фабрика репозитория возвращает конкретный тип
func NewPostgresUserRepo(pool *pgxpool.Pool) *UserRepo { ... }
Это упрощает тестирование (в тестах пробрасывается fake, реализующий интерфейс) и не плодит лишних обёрток.
Интерфейс — минимально маленький¶
Интерфейсы определяются на стороне consumer'а. Если AuthService использует
только Get и Create, то интерфейс UserRepo содержит только эти два
метода, даже если PostgresUserRepo умеет больше.
Struct'ы¶
Zero-value должен быть usable — где это разумно¶
// хорошо: можно создать &Limiter{} и работать с дефолтами
type Limiter struct {
perMinute int // 0 = unlimited
}
// плохо: zero-value упадёт при первом вызове
type HTTPClient struct {
client *http.Client // обязан быть не nil
}
Если zero-value нежизнеспособен — скрой struct и требуй конструктор
(NewHTTPClient).
Конструкторы NewXxx¶
Любой тип с нетривиальной инициализацией получает NewXxx:
func NewAuthService(
db TxRunner,
users UserRepo,
sessions SessionRepo,
publisher Publisher,
signer *token.Signer,
) *AuthService {
return &AuthService{ /* ... */ }
}
Конструктор: - валидирует обязательные параметры (возвращай error, если что-то nil), - задаёт дефолты (timeout, batch size), - не делает I/O (не шлёт запросы, не создаёт файлы).
Global state¶
- Глобальные переменные запрещены кроме
main.goи объявления sentinel-ошибок (var ErrX = errors.New(...)). - Никаких
var DB *pgxpool.Poolвinternal/. Зависимость передаётся через конструктор. - Никакого
init()для настройки глобальных синглтонов.init()допустим только для регистрации чего-то в сторонней библиотеке (например,database/sqlдрайвер) — и то лучше обойтись без этого.
Concurrency¶
- Одна goroutine — один владелец канала. Закрывает канал тот, кто пишет.
- Никаких
go func()без явного плана, как эта goroutine завершится. - Все long-running goroutine принимают
ctxи завершаются поctx.Done(). - При fan-out используй
errgroup.WithContextилиsync.WaitGroup+ буферизованный канал ошибок. - Никаких
time.Sleepкак способа синхронизации. Если ждёшь события — используй канал илиcontext.WithTimeout.
Тестируемость¶
- Service-методы принимают интерфейсы (см. выше) — это уже даёт тестируемость без mock-библиотек.
- Избегай time.Now() прямо в коде: заведи поле
clock func() time.Timeв struct, дефолтомtime.Now. В тестах подставляется фиксированное время. - Не используй глобальный rand. Заведи
*rand.Randв struct.
TODO / FIXME¶
TODOдопустим, только если рядом есть ссылка на issue:// TODO(#123): ....FIXMEбез issue — блокирует merge. Ревьюер имеет право потребовать либо починку, либо ссылку на issue, либо удаление FIXME.- Не оставляй закомментированный код. Удаляй — история есть в git.
Форматирование¶
gofmt/goimports— обязателен. Запускается pre-commit hook'ом.- Длина строки — мягкий лимит 120 символов. Жёсткого лимита нет, но если строка длинная из-за большого количества аргументов — переноси каждый аргумент на отдельную строку.
- Блок импортов — три группы через пустую строку: stdlib → внешние →
внутренние проектные.
goimportsделает это автоматически.