JavaRush /Курсы /Go SELF /Релизный ритуал: fmt/test/vet/lint + smoke‑сценарии

Релизный ритуал: fmt/test/vet/lint + smoke‑сценарии

Go SELF
55 уровень , 4 лекция
Открыта

1. Зачем нужен релизный ритуал

Когда вы только учитесь, кажется, что «работает на моём компьютере» — это почти релиз. Но реальность сурова: код может запускаться у вас и падать у пользователя, потому что вы забыли отформатировать файлы, пропустили тест, случайно сломали контракт вывода или не заметили предупреждение vet. Релизный ритуал — это не магия и не бюрократия, а последовательность одинаковых проверок, которые вы делаете перед тем, как сказать миру: «вот эту версию можно брать».

Представьте, что вы выпускаете маленький CLI‑инструмент (наш учебный таск‑менеджер). Для пользователя важно три вещи: бинарник запускается, команды работают, ошибки понятные и exit codes стабильные. Для вас как разработчика важно ещё одно: если что-то сломалось, вы узнаёте об этом до релиза, а не из гневного сообщения «у вас в 3:00 ночи всё упало».

Ритуал хорош тем, что он повторяемый. В идеале у вас есть один “конвейер” (локально и в CI), и он всегда делает одно и то же.

Нарисуем идею конвейера:

flowchart TD
    A[Изменили код] --> B[gofmt / goimports]
    B --> C[go test ./...]
    C --> D[go vet ./...]
    D --> E[линтеры]
    E --> F[сборка]
    F --> G[smoke-сценарии]
    G --> H[релиз]

Смысл простой: если вы не прошли ранний шаг, поздние делать бессмысленно. Форматирование не прошло — тесты всё равно будут спорить с ревьюером. Тесты упали — smoke‑сценарии вам ничего не гарантируют.

2. Quality gate: что проверяем и почему важен порядок

Перед релизом полезно думать не «что бы ещё запустить», а «какие риски я закрываю». У каждого шага ритуала есть свой тип проблем, которые он ловит. Форматирование ловит хаос в стиле и косвенно предотвращает бессмысленные конфликты в Git. Тесты ловят логические регрессии и поломки контракта функций. go vet ловит подозрительные места, которые компилируются, но пахнут потенциальной бедой (например, неверные Printf‑форматы или ошибки в тестовой инфраструктуре). Линтеры ловят “вроде бы нормально, но лучше не надо”: мёртвый код, странные конструкции, потенциальные баги, которые не всегда очевидны в ревью.

Важно, что порядок экономит время и нервы. Если вы запускаете линтер до форматирования, то в отчёте будете читать много мусора и пропустите главное. Если запускаете smoke‑сценарии до тестов, вы можете «прокликать» счастливый путь и всё равно выпустить версию, которая ломается в углах.

Удобно держать это в одной таблице:

Шаг Команда (идея) Что защищает Если упало — что делать
Формат
gofmt / goimports
читаемость, стабильные diffs форматируем и коммитим изменения
Тесты
go test ./...
регрессии логики, контракт вывода чиним код или тест, добавляем кейс
Vet
go vet ./...
“компилируется, но опасно” исправляем предупреждение, не игнорируем “на потом”
Линтер
staticcheck / golangci-lint
качество, риск скрытых багов исправляем или осознанно подавляем правило
Smoke ручные сценарии “живое” ощущение продукта фиксируем UX, контракты stdout/stderr, exit codes

В этой лекции мы не будем превращать это в религиозную войну «какой линтер лучше». Мы фиксируем ритуал как привычку: один и тот же порядок, одни и те же ожидания.

3. Форматирование: gofmt как часть языка

Если вы раньше писали на языках, где стиль кода обсуждают дольше, чем архитектуру, gofmt выглядит как спасательный круг: он просто говорит «делаем так», и всё. В Go форматирование — это не личный выбор и не “код‑стайл команды”, а базовый стандарт экосистемы. Поэтому в релизном ритуале форматирование стоит первым шагом: оно дешёвое, быстрое, и оно устраняет шум до того, как вы начнёте искать реальные баги.

Практически это означает, что перед релизом вы обязаны убедиться, что репозиторий уже отформатирован. В большинстве команд это делается автоматически (IDE, pre‑commit hook), но ритуал тем и хорош, что не требует веры в “оно само”.

Чтобы почувствовать, зачем это нужно, вспомним: Go очень любит, когда код читается одинаково. Иначе вы тратите мозг на пробелы, а не на смысл. А мозг в программировании и так занят более важным — например, почему тест падает только на CI.

Мини‑пример из нашего CLI‑приложения. Представьте, что кто-то написал так:

package apperr

type AppError struct{Kind Kind;Op string;Err error}

Это компилируется. Но это выглядит так, будто автор писал код в лифте между этажами. После gofmt будет нормально:

package apperr

type AppError struct {
	Kind Kind
	Op   string
	Err  error
}

Ритуал здесь очень простой: перед релизом вы приводите проект к состоянию, где gofmt ничего не меняет. Тогда следующий шаг — тесты — не будет “тонуть” в бессмысленных диффах.

4. Тесты: go test как страховка контракта

Тесты в Go — это не «дополнительная опция для перфекционистов», а нормальный способ не выпускать регрессию каждые полчаса. Особенно в CLI‑приложении, где важно не только “правильно посчитать”, но и сохранить договор: что печатаем в stdout, что — в stderr, какие exit codes возвращаем. Сам инструмент go test под капотом строит тестовый раннер и запускает пакетные тесты автоматически, то есть вам не нужно писать “main для тестов” вручную.

Для релизного ритуала важна простая мысль: релиз без зелёных тестов — это не смелость, это азартная игра. А казино, как известно, почти всегда выигрывает.

Отдельно полезно помнить про example‑тесты: это примеры, которые одновременно являются тестами и документацией. Они запускаются через go test и сверяют stdout с комментарием // Output:. Это очень подходит для CLI‑логики, где мы хотим зафиксировать формат сообщений и не сломать его случайным рефакторингом.

Давайте сделаем маленький example‑тест для нашей функции пользовательского сообщения (условно apperr.UserMessage). Код короткий, но он фиксирует формат:

package apperr_test

import (
	"errors"
	"fmt"

	"example/apperr"
)

func ExampleUserMessage_validation() {
	err := apperr.Validation("parse args", errors.New("missing -title"))
	fmt.Println(apperr.UserMessage(err)) // invalid input: missing -title
	// Output: invalid input: missing -title
}

Обратите внимание на важный психологический эффект. Тест не “доказывает математически”, что всё хорошо, но он ловит самое опасное: вы поменяли формат, скрипты начали парсить иначе, пользователи заметили раньше вас. Ритуал перед релизом гарантирует, что такие тесты прогоняются всегда.

5. go vet: когда компилятор молчит, но здравый смысл кашляет

go vet — это не линтер в стиле “мне не нравится ваш код”. Это инструмент, который ищет частые опасные ошибки и подозрительные конструкции. Его главная сила в том, что он ловит вещи, которые компилируются, но могут вести к сюрпризам. Перед релизом это особенно важно: такие баги любят проявляться у пользователей, а не у автора, потому что пользователь всегда найдёт самый неловкий ввод и самую неподходящую платформу.

Хороший пример — форматирование Printf. Код может работать “вроде бы”, но вывод окажется неверным или вообще сломается в рантайме. vet умеет подсвечивать несоответствие плейсхолдеров и аргументов. Это не гарантирует, что вы всегда правы, но сильно сокращает число глупых ошибок.

Мини‑пример (он компилируется, но vet будет недоволен):

package main

import "fmt"

func main() {
	name := "Alice"
	fmt.Printf("hello %d\n", name) // vet: %d expects integer
}

Отдельно приятно, что в новых версиях Go vet становится умнее именно в области тестов: появляются анализаторы, которые помогают находить типичные ошибки в объявлениях тестов, бенчмарков, fuzz‑тестов и example‑тестов. Это важно для релиза, потому что сломанный тест может выглядеть как “тест просто не запустился”, и вы ошибочно подумаете, что всё зелёное.

Ритуальное правило здесь скучное и полезное: если go vet ругается, мы не “запоминаем и потом поправим”, а исправляем сейчас. Перед релизом “потом” обычно означает “после того, как пользователи нашли баг”.

6. Линтеры: защита от медленных будущих проблем

После gofmt, go test и go vet часто хочется сказать: “ну всё, хватит, я устал”. Это нормальное чувство. Но линтеры полезны тем, что ловят проблемы, которые редко взрываются прямо сейчас, зато отлично копят технический долг. Они находят не только ошибки, но и сигналы: «этот код подозрительный», «это условие всегда true», «вы делаете лишнюю работу», «вы игнорируете возвращаемую ошибку».

Если vet — это нотариус (проверяет очевидные юридические косяки), то линтер — это дотошный редактор, который говорит: “вот здесь вас можно понять неправильно, а вот тут через полгода вы сами себя проклянёте”.

Пример “мелочи”, которая не ломает программу, но неприятна. Допустим, кто-то в нашем CLI‑коде сделал так:

package main

import "fmt"

func renderHeader() string {
	return fmt.Sprintf("Tasks") // лишний Sprintf, можно просто "Tasks"
}

Это не преступление. Но в большом проекте таких “мелочей” становится сотни, и код начинает выглядеть как чердак: вроде всё нужно, но пройти невозможно.

Для релизного ритуала важна дисциплина: линтер должен запускаться одинаково всегда, а не “когда настроение”. И очень важно не впасть в крайность: если линтер предлагает спорную правку, вы должны либо исправить код так, чтобы он стал яснее, либо осознанно принять решение и, если нужно, задокументировать подавление правила. Иначе линтер превращается в генератор раздражения, а не качества.

7. Smoke‑сценарии: быстрый прогон по живому

Smoke‑тесты (или smoke‑сценарии) — это короткий набор проверок, которые подтверждают, что приложение запускается и выполняет ключевые команды. Это не нагрузочное тестирование, не “пройти все ветки кода”, и не попытка заменить unit‑тесты. Это скорее вопрос: “если я дам бинарник человеку, он вообще сможет сделать базовые вещи, не впадая в отчаяние?”

Почему smoke‑сценарии нужны, если у нас есть тесты? Потому что тесты обычно проверяют детали (функции, методы, форматы), а smoke‑сценарий проверяет «склейку»: бинарник собрался, флаги парсятся, help печатается туда, куда нужно, exit code действительно совпадает с политикой, а ошибки не утекли в stdout.

Самое полезное в smoke‑сценариях — фиксировать ожидания максимально конкретно: какие команды запускать и что примерно должно получиться (хотя бы по exit code и по наличию ключевых строк).

Ниже пример smoke‑таблицы для нашего CLI‑таск‑менеджера (назовём бинарник todo). Важно: это не список “идей”, а маленький договор перед релизом.

Сценарий Команда Что ожидаем
Help работает
todo -h
exit code 0, usage в stdout или stderr (как вы договорились), без паники
Ошибка использования
todo add
(без -title)
exit code 2, короткое "invalid input …" в stderr
Добавление задачи
todo add -title "buy milk"
exit code 0, понятный результат в stdout
Список задач
todo list
exit code 0, стабильный формат (таблица/строки)
Mark done
todo done -id 1
exit code 0, задача помечена
Не найдено (если различаете)
todo done -id 99999
exit code 1 или детализированный код (если вы включили), сообщение без внутренностей
Версия (если есть)
todo --version
exit code 0, версия печатается стабильно

Заметьте, smoke‑сценарии специально короткие. Их сила не в глубине, а в регулярности. Перед релизом вы делаете этот “прогон” и сразу видите: “о, я случайно начал печатать ошибки в stdout” или “я убрал флаг -h и теперь люди не понимают, как пользоваться”.

Команда doctor как опора для smoke‑прогона

Иногда хочется, чтобы smoke‑прогон был ещё проще: одна команда, которая проверяет базовую жизнеспособность окружения. Например, что конфиг валиден и файл данных доступен. Такая команда часто называется doctor, check, selftest. Она не заменяет тесты и не заменяет CI, но для релиза удобна: вы запускаете todo doctor и сразу видите “минимум жив”.

Чтобы не превращать это в отдельный продукт, сделаем команду очень простой: проверим, что путь к файлу базы задач существует (или что директория существует), и если нет — вернём KindValidation (exit code 2) с понятным сообщением.

Небольшой пример функции, которая вписывается в наш стиль AppError и не размазывает os.Exit по коду:

package main

import (
	"errors"
	"os"

	"example/apperr"
)

func runDoctor(dataPath string) error {
	if dataPath == "" {
		return apperr.Validation("doctor", errors.New("missing data path"))
	}
	if _, err := os.Stat(dataPath); err != nil {
		return apperr.Validation("doctor", errors.New("data file is not accessible"))
	}
	return nil
}

Да, это нарочно упрощено. Мы не различаем “файла нет” и “нет прав” — для базового smoke‑сценария это нормально: пользователь всё равно должен получить короткое “не могу работать с файлом”. Детали останутся в логах, если вы завернёте первопричину аккуратнее.

Теперь в smoke‑таблицу можно добавить строку:

Сценарий Команда Что ожидаем
Проверка окружения
todo doctor -data ./tasks.json
exit code 0 при корректном окружении, иначе 2

И вот здесь ритуал начинает работать как система: тесты гарантируют, что ExitCode и UserMessage не сломаны, vet гарантирует отсутствие типичных ловушек, линтеры чистят шероховатости, а doctor помогает быстро проверить бинарник “вживую”.

8. Типичные ошибки перед релизом

Перед релизом люди часто совершают одни и те же ошибки не потому, что они “плохие разработчики”, а потому что мозг устал, хочется быстрее, и кажется, что «и так сойдёт». Ритуал как раз нужен, чтобы спасать вас от вас же — особенно в пятницу вечером.

Ошибка №1: релиз без чистого gofmt, потому что “это не влияет на работу”.
Формально правда: программа запустится. Практически — вы создаёте лишний шум в diff’ах, усложняете ревью, а потом внезапно ловите конфликт форматирования в самый неподходящий момент. Это та боль, которую легко убрать одним ранним шагом.

Ошибка №2: “тесты долго идут, я проверил руками”.
Ручная проверка ловит счастливый путь, но не ловит регрессии в углах. Тесты, особенно table‑driven и example‑тесты, нужны именно для фиксации контрактов. В Go примеры могут быть тестами с проверяемым stdout, так что “документация не устаревает” — это не лозунг, а реальный механизм.

Ошибка №3: игнорировать go vet, потому что “это же не ошибка компиляции”.
vet часто ругается на вещи, которые сегодня “не взорвались”, но завтра выстрелят в ногу. Особенно неприятно, когда это касается форматирования вывода или тестовой инфраструктуры. С учётом того, что go vet развивается и получает новые анализаторы для тестов, игнорировать его перед релизом — плохая инвестиция.

Ошибка №4: превращать линтер в врага и выключать его целиком.
Линтеры должны помогать, а не унижать. Если он шумит, значит либо выбран слишком агрессивный набор правил, либо код действительно стал грязным. Правильная стратегия — настроить разумный минимум и поддерживать его стабильным, а не выключать всё “до лучших времён”.

Ошибка №5: smoke‑сценарии без фиксации ожиданий.
Такой smoke‑прогон не повторяем и не даёт уверенности. Лучше иметь маленькую таблицу из 5–8 команд с ожидаемыми exit codes и ключевыми строками, чем “пятиминутный шаманизм”.

1
Задача
Go SELF, 55 уровень, 4 лекция
Недоступна
Релизная шпаргалка
Релизная шпаргалка
1
Задача
Go SELF, 55 уровень, 4 лекция
Недоступна
Контракт примера
Контракт примера
1
Задача
Go SELF, 55 уровень, 4 лекция
Недоступна
Строка табло
Строка табло
1
Задача
Go SELF, 55 уровень, 4 лекция
Недоступна
Команда доктора
Команда доктора
1
Опрос
Стандартизация ошибок, 55 уровень, 4 лекция
Недоступен
Стандартизация ошибок
Стандартизация ошибок
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ