Опирается на правила: GO-8.1GO-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.1gofmt 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 биндинга.