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‑сценарии до тестов, вы можете «прокликать» счастливый путь и всё равно выпустить версию, которая ломается в углах.
Удобно держать это в одной таблице:
| Шаг | Команда (идея) | Что защищает | Если упало — что делать |
|---|---|---|---|
| Формат | |
читаемость, стабильные diffs | форматируем и коммитим изменения |
| Тесты | |
регрессии логики, контракт вывода | чиним код или тест, добавляем кейс |
| Vet | |
“компилируется, но опасно” | исправляем предупреждение, не игнорируем “на потом” |
| Линтер | |
качество, риск скрытых багов | исправляем или осознанно подавляем правило |
| 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 работает | |
exit code 0, usage в stdout или stderr (как вы договорились), без паники |
| Ошибка использования | (без -title) |
exit code 2, короткое "invalid input …" в stderr |
| Добавление задачи | |
exit code 0, понятный результат в stdout |
| Список задач | |
exit code 0, стабильный формат (таблица/строки) |
| Mark done | |
exit code 0, задача помечена |
| Не найдено (если различаете) | |
exit code 1 или детализированный код (если вы включили), сообщение без внутренностей |
| Версия (если есть) | |
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‑таблицу можно добавить строку:
| Сценарий | Команда | Что ожидаем |
|---|---|---|
| Проверка окружения | |
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 и ключевыми строками, чем “пятиминутный шаманизм”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ