Опирается на правила:
GO-8.1…GO-8.5иGO-8.X1из Go Style Guide → раздел 8. Форматирование и стиль.
Важно знать
gofmtиgoimportsзапускаются до коммита — форматирование не обсуждается вручную (GO-1.2).- CI блокирует на
gofmt -l .с выводом списка отклонившихся файлов (GO-8.1).- Длина строки — ориентир 100–120 символов; фиксируется через
lllв.golangci.yml(GO-8.2).- Горизонтальное выравнивание через пробелы — запрещено: шумит в diff при переименовании поля (GO-8.X1).
- Blank lines: одна пустая строка между логическими блоками функции, не более одной подряд (GO-8.4).
const-блок для перечислений;iotaне перекрывается между разнымиconst-блоками (GO-8.5).- Множественное присваивание в одну строку — только для связанных значений вроде
x, y := f()(GO-8.3).
Форматирование в Go решено на уровне инструментария — gofmt устраняет целый класс ревью-споров. Задача команды: настроить пайплайн один раз и держать CI зелёным, а не обсуждать отступы на каждом PR.
gofmt и goimports — обязательный минимум
GO-8.1: gofmt и goimports запускаются локально (hook или IDE on-save) и в CI. Если gofmt -l . выводит имена файлов — CI завершается с ошибкой.
# проверка в CI (Go-style guide шаг 8.1)
gofmt_out=$(gofmt -l .)
if [ -n "$gofmt_out" ]; then
echo "gofmt: следующие файлы не отформатированы:"
echo "$gofmt_out"
exit 1
fi
goimports дополняет gofmt: он ещё группирует блоки импортов (stdlib / external / internal) с пустой строкой между группами и удаляет неиспользуемые импорты. Подробнее о группировке — в Пакеты и импорты.
gofumpt — более строгий форматтер поверх gofmt; применяется через golangci-lint с флагом gofumpt: true. Вводить в проекте имеет смысл при старте — ретроспективный запуск на большой кодовой базе даёт шумный diff.
Длина строки — 100–120 символов
GO-8.2: командное соглашение фиксируется в .golangci.yml через линтер lll:
# .golangci.yml
linters-settings:
lll:
line-length: 120
linters:
enable:
- lll
Граница 120 символов — осмысленный ориентир для Go: идентификаторы короче Java (нет com.example.service.OrderConfirmationService), зато функциональные опции и struct-литералы растут горизонтально.
При превышении — решение в одном из трёх направлений:
// 1. struct-литерал: переносим поля
order := Order{
ID: "ORD-001",
CustomerID: "CUST-42",
Status: StatusConfirmed,
TotalCents: 15000,
}
// 2. длинный вызов с опциями: переносим аргументы
client, err := NewOrderClient(
WithBaseURL(cfg.CatalogURL),
WithTimeout(cfg.ClientTimeout),
WithRetry(3),
)
// 3. длинная цепочка условий: guard clause
if order.Status != StatusConfirmed ||
order.TotalCents <= 0 ||
order.CustomerID == "" {
return nil, ErrOrderInvalid
}
gofmt сам расставляет отступы после переноса — не нужно выравнивать вручную.
Множественное присваивание — только связанные значения
GO-8.3: несколько переменных в одну строку допустимо, только если значения семантически связаны.
// ХОРОШО — обе переменные из одного вызова
order, err := repo.FindByID(ctx, orderID)
// ХОРОШО — координаты
x, y := parseCoordinates(raw)
// ПЛОХО — несвязанные переменные ради краткости
customerID, productSKU := req.CustomerID, req.ProductSKU // не одна операция
Для несвязанных переменных — отдельные строки. Код читается сверху вниз, и группировка несвязанного в одну строку заставляет читателя дважды проверять, что тут происходит.
Blank lines между логическими блоками
GO-8.4: одна пустая строка разделяет логически самостоятельные блоки внутри функции. Более одной пустой строки подряд — не допускается.
func (s *OrderService) ConfirmOrder(ctx context.Context, orderID string) (*Order, error) {
order, err := s.repo.FindByID(ctx, orderID)
if err != nil {
return nil, fmt.Errorf("confirm order: %w", err)
}
if err := order.Confirm(); err != nil {
return nil, fmt.Errorf("confirm order: %w", err)
}
if err := s.repo.Save(ctx, order); err != nil {
return nil, fmt.Errorf("save confirmed order: %w", err)
}
return order, nil
}
Три логических шага (загрузка → бизнес-правило → сохранение) разделены одной пустой строкой каждый. Читается как три абзаца, а не один монолит.
Между top-level объявлениями — gofmt расставляет пустые строки автоматически согласно Go-конвенции.
const-блок и iota
GO-8.5: перечисления собираются в один const-блок; iota не перекрывается между блоками.
// ХОРОШО — все значения статуса в одном блоке
type OrderStatus int
const (
StatusDraft OrderStatus = iota + 1
StatusConfirmed
StatusShipped
StatusCancelled
)
// ПЛОХО — iota в двух блоках даёт одинаковые числовые значения
const (
StatusDraft OrderStatus = iota + 1
StatusConfirmed
)
const (
StatusShipped OrderStatus = iota + 1 // ← снова 1, не 3!
StatusCancelled
)
Разрыв const-блока создаёт скрытую ошибку: iota сбрасывается в каждом новом блоке. StatusShipped получит значение 1, совпадающее со StatusDraft. Компилятор не предупредит — это легальный Go.
Для строковых enum можно использовать stringer (go generate):
// core/order/status.go
//go:generate stringer -type=OrderStatus
type OrderStatus int
const (
StatusDraft OrderStatus = iota + 1
StatusConfirmed
StatusShipped
StatusCancelled
)
stringer генерирует метод String(), что позволяет логировать статус читаемо через slog:
slog.InfoContext(ctx, "order status changed",
"orderID", order.ID,
"status", order.Status,
)
Горизонтальное выравнивание через пробелы запрещено
GO-8.X1: выравнивание полей структуры или map-литерала пробелами шумит в diff.
// ПЛОХО — выравнено пробелами
type Product struct {
ID string
Name string
PriceCents int64
StockCount int
CategoryID string
}
// ХОРОШО — без выравнивания
type Product struct {
ID string
Name string
PriceCents int64
StockCount int
CategoryID string
}
При добавлении поля с более длинным именем — например DiscountPercent int — «выравненный» вариант требует переформатировать все строки блока. Diff показывает 5 изменений вместо 1. golangci-lint с настройкой gocritic ловит избыточные пробельные выравнивания.
gofmt выравнивает поля структур табами, а не пробелами — это ок и согласовано между разработчиками автоматически.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Коммит без gofmt или CI без проверки gofmt -l . | GO-8.1 | gofmt on-save + CI блок |
Строки >120 символов (конфигурация lll) | GO-8.2 | перенос аргументов / struct-литерала |
a, b, c := x, y, z для несвязанных переменных | GO-8.3 | отдельные строки присваивания |
| Две и более пустые строки подряд внутри функции | GO-8.4 | одна пустая строка — один разделитель |
iota в двух отдельных const-блоках для одного типа | GO-8.5 | один const-блок на перечисление |
| Горизонтальное выравнивание через пробелы | GO-8.X1 | без выравнивания; gofmt расставит табы |
//nolint:lll без обоснования | GO-LINT-6 | комментарий «зачем» обязателен |
Куда дальше
- Именование — длинные или короткие имена часто причина длинных строк; конвенции Go-пакетов.
- Пакеты и импорты — группировка блоков импортов через
goimports. - golangci-lint — полная конфигурация
.golangci.yml:lll,gocritic,revive,errcheck. - Enforcement через golangci-lint — нормативные формулировки раздела 11 биндинга.