1. Зачем нужен демо‑сценарий на 5–10 минут
Если вам когда‑нибудь показывали проект «сейчас я вам всё объясню» и через 20 минут вы всё ещё смотрели на структуру папок — вы понимаете боль. Демо‑сценарий — это способ не превращать показ результата в экскурсию по коду (код мы и так любим, но не все обязаны разделять это чувство). Формат «5–10 минут» дисциплинирует: вы показываете только то, что даёт ценность и доверие.
Важная мысль: хорошее демо — это не «что у нас есть», а «что пользователь может сделать и на что он может рассчитывать». Поэтому демо почти всегда строится в порядке поведение → контракты → сигналы качества. Поведение — это наблюдаемое действие (команда CLI или HTTP-запрос). Контракты — это стабильные правила (stdout/stderr, exit codes, статусы HTTP, error envelope). Сигналы качества — это «почему этому можно доверять» (стабильность вывода, тесты, неутечка внутренних ошибок, request_id и т.п.).
Чтобы не быть голословными, в этой лекции будем считать, что у нас есть учебное приложение Tasker: менеджер задач с CLI и HTTP API. Мы не будем «дописывать функциональность» (курс уже финализируется), мы будем учиться показывать то, что вы уже построили.
Мини‑схема: что именно мы показываем
flowchart TD
A[Демо начинается с поведения] --> B[CLI: команды и exit codes]
B --> C[HTTP: статусы и JSON-контракт]
C --> D[Сигналы качества: тесты, стабильность, request_id]
D --> E[Вопросы / финальная проверка ожиданий]
2. Собираем демо как одну историю
Самая частая проблема начинающих — желание показать весь проект сразу, потому что «я же старался». Это нормально (мы все хотим, чтобы нас погладили по коду), но демо работает иначе: это короткая история с началом, действием и концом. Как в хорошем анекдоте: если надо объяснять 10 минут — это уже не анекдот, а лекция по истории юмора.
Сценарий удобно держать в голове как мини‑протокол:
- у вас есть исходное состояние (пустой список задач или несколько задач),
- вы выполняете 2–4 действия,
- каждое действие даёт наблюдаемый результат,
- вы один раз показываете, как система ведёт себя при ошибке,
- вы заканчиваете на «успешном» финале (задача создана/выполнена/получена по API).
Чтобы это было проще повторять, удобно сделать демо в виде «таймлайна». Не списком в воздухе, а таблицей — она дисциплинирует и вас, и рассказ.
| Время | Шаг демо | Что запускаем | Что проверяем глазами |
|---|---|---|---|
| 0:00–0:30 | Pitch | речь (3–5 предложений) | ясно, что за проект и зачем |
| 0:30–2:30 | CLI happy path | |
stdout = результат, exit code = 0 |
| 2:30–3:30 | CLI ошибка | неправильная команда/аргумент | stderr = ошибка/usage, exit code = 2 |
| 3:30–6:30 | HTTP happy path | |
статусы 200/201, JSON корректен |
| 6:30–8:00 | HTTP ошибка | + error envelope |
единый формат ошибок, fields для validation |
| 8:00–10:00 | Сигналы качества | |
проект «взрослый», не только «работает» |
В этой лекции мы разберём, как быстро подготовить каждую часть, и какие маленькие кусочки кода помогают удерживать контракт (чтобы ваш проект не превращался в «оно сегодня так себя ведёт, а завтра — посмотрим»).
3. Pitch: короткое вступление
Смешно, но факт: люди чаще всего «проваливают» демо не на коде и даже не на баге, а на первом объяснении. Когда вы начинаете с архитектуры и папок, слушатель не понимает, что вообще происходит, и мозг уходит «в энергосбережение». Pitch нужен, чтобы мозг слушателя понял: «а, я понял задачу и критерии качества».
Pitch должен быть коротким и скучным (в хорошем смысле). Пример для Tasker:
Tasker — это маленький менеджер задач. У него есть CLI и HTTP API.
В CLI я покажу контракт stdout/stderr и коды возврата 0/1/2, а в HTTP — статусы и единый JSON‑формат ошибок.
Для неожиданных проблем наружу не утекают внутренние детали: 500‑класс отдаёт стабильное сообщение.
Плюс покажу request_id как минимальный сигнал наблюдаемости.
Обратите внимание: вы заранее называете «сигналы качества». Это не хвастовство, это навигация: слушатель начинает смотреть туда, куда нужно. И вам легче не сбиться в «сейчас ещё вот это покажу».
4. Демо CLI: stdout/stderr и коды выхода
CLI‑демо удобно делать первым: оно быстрое, локальное и понятное. Здесь вы выигрываете тем, что сразу показываете дисциплину: stdout — результат, stderr — ошибки/usage, exit codes — контракт. В реальном мире это позволяет людям писать скрипты, а тестам — быть стабильными. В учебном мире это позволяет преподавателю понять, что вы не «вручную глазами проверяете», а проектируете интерфейс.
Ключевой трюк — сделать центральную функцию run(args) и в main() просто вызвать os.Exit(code). Это банально, но очень полезно: так вы не расползаетесь по log.Fatal и «выходам из середины программы», а держите управление в одном месте.
Мини‑пример: каркас CLI main → run → exit
package main
import (
"os"
)
func main() {
os.Exit(run(os.Args[1:]))
}
Этот фрагмент маленький, но он задаёт стиль: всё, что печатает/парсит/ошибается, должно быть внутри run.
Мини‑пример: базовый диспетчер команд
package main
import (
"fmt"
"os"
)
func run(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: tasker <command>")
return 2
}
if args[0] == "help" || args[0] == "-h" || args[0] == "--help" {
fmt.Println("usage: tasker <command>") // stdout: это нормальный результат
return 0
}
fmt.Fprintln(os.Stderr, "unknown command:", args[0])
return 2
}
Здесь вы демонстрируете сразу два договора: help — это успех (код 0), а ошибка ввода — это usage (код 2). Это выглядит мелочью, пока вы не начинаете писать тесты на CLI и запускать команды в bash. Тогда внезапно оказывается, что «мелочи» — это и есть UX.
Что показывать в демо CLI
Вместо десяти команд покажите три:
- «Создать задачу» (happy path).
- «Показать список» (стабильный вывод, сортировка/порядок — если есть).
- «Сломать ввод» (ошибка, exit code 2, stderr).
Пример «как это может выглядеть» (это не код, а сценарий показа):
- tasker add -title "Buy milk" → stdout: created task id=1
- tasker list → stdout: таблица или строки
- tasker add (без title) → stderr: title is required, exit code 2
В этот момент вы словами фиксируете контракт: «Результат я печатаю в stdout, ошибки и подсказки — в stderr. По кодам: 0 — успех, 2 — проблема с аргументами, 1 — ошибка выполнения». И вы уже выглядите человеком, который пишет софт, а не «набор команд, который я запускаю руками».
5. Демо HTTP: статусы и error envelope
После CLI логично перейти к HTTP: здесь вы показываете, что те же идеи (контракты, предсказуемость, ошибки) масштабируются на сетевой интерфейс. В HTTP особенно важно не расползаться: если вы начнёте рассказывать про middleware, слои и DI, вы потеряете 5–10 минут ещё до первого запроса. Поэтому держим фокус: запрос → статус → тело → контракт ошибки.
В этом курсе мы заранее договорились про единый формат ошибок в JSON: error envelope. Это как форма для документов: пусть скучная, но зато любой клиент знает, куда смотреть.
Мини‑пример: структуры error envelope
package api
type ErrorResponse struct {
Error ErrorBody `json:"error"`
}
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
Fields map[string]string `json:"fields,omitempty"`
}
Эта структура должна быть «каменной»: если вы меняете её каждый раз по настроению, тестировать и поддерживать API будет больно.
Мини‑пример: единый writeError без утечки внутренностей
Очень важная инженерная привычка: наружу не отдаём «внутреннюю правду» на 500-класс ошибок. У пользователя должна быть стабильная, безопасная формулировка. Внутренние детали — для логов. Это напрямую связано с тем, что в Go ошибки — значения, и вы часто добавляете контекст по мере продвижения вверх по стеку: контекста может быть много, и не всё из него нужно показывать клиенту.
package api
import (
"encoding/json"
"net/http"
)
func writeError(w http.ResponseWriter, status int, code, msg string, fields map[string]string) {
if status >= 500 {
code = "internal"
msg = "internal error"
fields = nil
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(ErrorResponse{
Error: ErrorBody{Code: code, Message: msg, Fields: fields},
})
}
В демо вы прямо проговариваете: «Внутреннюю ошибку я не показываю наружу, потому что это и безопасность, и стабильность контракта». Это один из самых сильных «сигналов качества», который можно показать за 10 секунд.
Как показывать HTTP вживую
Вам нужны 3–4 запроса. Например:
- GET /health → 200 OK → текст ok или JSON (что выбрали).
- POST /api/v1/tasks → 201 Created → JSON с задачей и id.
- GET /api/v1/tasks/999 → 404 Not Found → error envelope.
- POST /api/v1/tasks с плохим JSON/пустым title → 400 Bad Request → error envelope + fields.
В демо не надо «идеально помнить curl». Можно держать в README готовые команды. Это не читерство, это забота о себе: вы показываете продукт, а не навыки набора текста.
6. Request ID как сигнал «продакшена»
Request ID — это минимальная наблюдаемость. Даже если у вас нет красивого трейсинга и метрик, один request_id уже позволяет связать «ошибка у клиента» с «строчкой в логах». И самое приятное: показать это можно за 15 секунд, а выглядит это очень убедительно.
В демо обычно достаточно сказать: «Я поддерживаю заголовок X-Request-ID. Если его прислали — использую его. Если не прислали — генерирую. И кладу в логи с ключом request_id». Вам даже не обязательно показывать генерацию (она может быть внутри middleware), но стандарты именования стоит зафиксировать.
Мини‑пример: константы для заголовка и логов
package api
const RequestIDHeader = "X-Request-ID"
const LogFieldRequestID = "request_id"
Почему это важно? Потому что без констант у вас в одном месте появится X-Request-Id, в другом X-REQUEST-ID, а в логах кто-то напишет reqId. И это тот самый «смерть от тысячи порезов», когда всё вроде работает, но поддерживать невозможно.
7. Сигналы качества и подготовка к показу
Сигналы качества — это наблюдаемые признаки, которые не требуют «поверить вам на слово». В демо их надо не просто иметь, а называть. Не «ну там тесты есть», а «вот команда, вот результат». В Go это особенно естественно: инструменты простые, а дисциплина читается по мелочам.
При этом важно не уходить в «show off». Демо — не соревнование «кто больше флагов go test знает». Лучше показать один‑два сильных сигнала.
Хороший набор на 5–10 минут выглядит так:
- go test ./... проходит (можно показать в конце одной строкой).
- Вывод CLI/HTTP стабилен (без случайного порядка).
- Ошибки контрактные: в CLI есть 0/1/2, в HTTP — статусы + envelope.
- Внутренние ошибки наружу не сливаются (5xx всегда internal error).
- Есть request_id как минимальная наблюдаемость.
Если вы хотите добавить ещё один штрих (только один!), покажите один unit‑тест на парсинг id или на маппинг ошибок в exit code. Это быстро и «по делу».
Мини‑пример: маппинг ошибки в exit code
package main
import "errors"
var ErrUsage = errors.New("usage")
func exitCode(err error) int {
if err == nil {
return 0
}
if errors.Is(err, ErrUsage) {
return 2
}
return 1
}
Это хороший стиль: даже если внутри вы возвращаете разные ошибки, наружу вы маппите их предсказуемо. Кстати, сама идея «ошибки как значения» — одна из базовых ментальных моделей Go, и тут она очень наглядно проявляется.
Техника безопасности демо: как не утонуть в проекте
Перед финалом я хочу проговорить одну «психологическую» вещь. Когда вы нервничаете, вы начинаете ускоряться, а вместе с ускорением начинаете показывать лишнее: «а ещё у меня тут пакет internal», «а вот middleware», «а вот диаграмма», «а сейчас я открою папку adapters». Это выглядит как попытка спрятать отсутствие результата за активностью.
Самый спокойный способ удержаться — заранее принять правило: в демо мы не открываем редактор кода. Вообще. Даже «на секундочку». Потому что как только вы открыли код, вы уже не показываете продукт. Вы показываете процесс разработки.
Если очень хочется всё-таки показать «что код аккуратный», можно сделать это косвенно: тесты проходят, формат ошибок стабилен, внутренности не утекают наружу, request_id согласован. Это не хуже, чем листать файлы, и при этом не требует веры.
8. Типичные ошибки при показе CLI/HTTP демо
Ошибка №1: демо превращается в рассказ про архитектуру, а не про поведение.
Это случается, когда вы начинаете с «вот у меня есть папка internal, а вот domain…». В итоге слушатель так и не понял, что проект делает. Исправляется просто: начните с команды CLI или curl‑запроса. Сначала результат, потом (если спросят) — устройство.
Ошибка №2: вы не фиксируете контракт stdout/stderr и exit codes, и всё выглядит случайным.
Если вы печатаете часть ошибок в stdout, часть в stderr, а help возвращает код 1, у слушателя возникает ощущение «оно просто запускается». Намного лучше один раз явно проговорить договор: stdout — результат, stderr — ошибки/usage; 0/2/1 — как базовый контракт.
Ошибка №3: в HTTP ошибки «плавают» по формату, потому что каждый handler пишет по‑своему.
Сегодня у вас {"message":"bad"}, завтра {"error":"bad"}, послезавтра http.Error. Клиенту от этого не легче. В демо достаточно показать один error envelope и один writeError, чтобы стало ясно: формат стабилен, тестируем, поддерживаем.
Ошибка №4: на 500‑класс вы отдаёте клиенту err.Error().
Это выглядит честно, но на практике почти всегда плохо: вы раскрываете внутренние детали и делаете контракт нестабильным. В Go ошибки часто оборачиваются и несут много контекста, и этот контекст полезен вам в логах, но не клиенту. В демо лучше показать обратное: для 5xx сообщение фиксированное, детали — в логах.
Ошибка №5: вы показываете только happy path, и создаётся ощущение «оно работает, пока не трогать».
Один контролируемый негативный сценарий (CLI usage или HTTP validation error) резко повышает доверие. Это как краш‑тест: не потому что вы любите аварии, а потому что вы заранее подумали о реальности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ