<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Все публикации подряд на Хабре [expanded by feedex.net]</title><link>https://habr.com/ru/articles/</link><description>Все публикации подряд на Хабре</description><atom:link href="https://feedex.net/feed/habr.com/ru/rss/articles/all/" rel="self"/><lastBuildDate>Mon, 27 Apr 2026 05:27:53 +0000</lastBuildDate><item><title>Почему Go-сервисы начинают тормозить без ошибок (и при чём тут goroutines). Часть 1</title><link>https://habr.com/ru/articles/1028264/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1028264</link><description>&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;time datetime="2026-04-27T05:27:53.000Z" title="2026-04-27, 05:27"&gt;4 минуты назад&lt;/time&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;9 мин&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div lang="ru"&gt;&lt;div id="post-content-body"&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Часто про Go говорят: &lt;em&gt;«это язык, где конкурентность почти бесплатная»&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;И знаете что? Это правда. Почти.&lt;/p&gt;&lt;p&gt;Но &lt;strong&gt;«почти»&lt;/strong&gt; — это самое опасное во всей истории, так как либо ты управляешь системой, либо она управляет тобой руками runtime'а.&lt;/p&gt;&lt;p&gt;В трёх статьях я разберу путь, через который проходит почти каждый Go-разработчик от наивного «я добавил &lt;code&gt;go&lt;/code&gt; — получил параллельность», до взрослого «я проектирую concurrency-систему с понятными границами».&lt;/p&gt;&lt;p&gt;Погнали.&lt;/p&gt;&lt;h3&gt;Иллюзия первая: «Горутины дешёвые — значит можно сколько угодно»&lt;/h3&gt;&lt;p&gt;Новички в Go рассуждают примерно так:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Горутина весит 2 КБ стека. Поток — 1 МБ. Значит, я могу создать 500 000 горутин вместо 2000 потоков. Отлично, пишу &lt;code&gt;go&lt;/code&gt; на каждый чих!&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;И локально это работает. Даже на нагрузочном тесте — работает, но в боевом сценарии под настоящим трафиком — сервис превращается в черепаху, но &lt;em&gt;без единой ошибки&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;Ниже приведен совершенно классический код:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func handleRequest(req Request) {
    go processAsync(req)  // Эмитируем асинхронность
    respondOK()
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Кажется, что код выглядит безобидно, однако, если присмотреться, здесь нет:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;лимита на количество одновременно работающих горутин&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;очереди&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;контроля завершения&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;backpressure'а&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Получается, что тут вы не управляете системой, а надеетесь, что runtime справится и он честно пытается.&lt;/p&gt;&lt;h4&gt;В метриках это выглядит примерно так:&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;Число горутин:      1000 → 10000 → 50000
Время ответа:       50ms → 200ms → 800ms
CPU:                30%  → 70%   → 95% (полезной работы — всё меньше)
Ошибки:             0    → 0     → 0&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть формально наш сервис &lt;strong&gt;жив&lt;/strong&gt;, но &lt;strong&gt;мёртв&lt;/strong&gt; для пользователя.&lt;/p&gt;&lt;h4&gt;Заглянем под капот&lt;/h4&gt;&lt;p&gt;Планировщик Go (GMP-модель: Goroutines, Machine, Processor) начинает страдать:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;G&lt;/strong&gt; (горутины) — их слишком много, очередь runqueue растягивается&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;M&lt;/strong&gt; (основные потоки ОС) — пытаются всё вывезти&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;P&lt;/strong&gt; (процессоры логические) — мечутся между горутинами чаще, чем выполняют полезную работу&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Если говорить простыми словами, то планировщик тратит больше времени на &lt;em&gt;переключение&lt;/em&gt; между задачами, чем на &lt;em&gt;выполнение&lt;/em&gt; самих задач, это как если бы вы меняли инструменты каждые 10 секунд вместо того, чтобы работать. Однако, здесь может быть использован паттерн &lt;strong&gt;worker pool&lt;/strong&gt; + &lt;strong&gt;ограниченная очередь&lt;/strong&gt;, варианты его реализации мы разберем в части 3. Код одной из реализаций приведу ниже:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;type WorkerPool struct {
    tasks chan func()
}
func (p *WorkerPool) worker() {
    for task := range p.tasks {
        task()
    }
}
func NewWorkerPool(workers int, queueSize int) *WorkerPool {
    pool := &amp;amp;WorkerPool{
        tasks: make(chan func(), queueSize),
    }
    for i := 0; i &amp;lt; workers; i++ {
        go pool.worker()
    }
    return pool
}
func (p *WorkerPool) Submit(task func()) error {
    select {
    case p.tasks &amp;lt;- task:
        return nil
    default:
        return ErrQueueFull // backpressure — важнейшая вещь
    }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И вот именно без этого вы не управляете системой, а идея в том, что мы &lt;strong&gt;перестаём создавать горутину на каждую задачу&lt;/strong&gt;, а заводим фиксированное количество воркеров, которые обрабатывают задачи из очереди.&lt;/p&gt;&lt;p&gt;У нас есть канал &lt;code&gt;tasks&lt;/code&gt; — это очередь задач. Туда мы будем складывать функции, которые нужно выполнить, далее создаём пул:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func (p *WorkerPool) worker() {
    for task := range p.tasks {
        task()
    }
}
func NewWorkerPool(workers int, queueSize int) *WorkerPool {
  pool := &amp;amp;WorkerPool{
    tasks: make(chan func(), queueSize),
  }
  for i := 0; i &amp;lt; workers; i++ {
    go pool.worker()
  }
  return pool
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Что здесь происходит:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;queueSize&lt;/code&gt; — это &lt;strong&gt;максимальный размер очереди&lt;/strong&gt; &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;workers&lt;/code&gt; — сколько задач может выполняться &lt;strong&gt;одновременно&lt;/strong&gt; &lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Мы поднимаем фиксированное количество горутин-воркеров, и дальше они просто живут и обрабатывают задачи из канала. Ключевой момент — мы больше &lt;strong&gt;не создаём бесконечное число горутин&lt;/strong&gt;, у нас есть потолок.&lt;/p&gt;&lt;p&gt;Теперь самое интересное — добавление задачи:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func (p *WorkerPool) Submit(task func()) error {
  select {
    case p.tasks &amp;lt;- task:
    return nil
    default:
    return ErrQueueFull
  }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вот здесь происходит магия - мы пытаемся положить задачу в канал и, если в очереди есть место — задача принимается, однако, если очередь заполнена — сразу получаем &lt;code&gt;ErrQueueFull&lt;/code&gt; и это принципиально важно.&lt;/p&gt;&lt;p&gt;Посмотрим на ту боль с кодом, который на первый взгляд рабочий и достойно ведет себя на тестовых стендах:&lt;/p&gt;&lt;h3&gt;Инцидент первый: «подождём секундочку »&lt;/h3&gt;&lt;p&gt;Код ниже, ломает 50% наивных реализаций:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func main() {
    go doWork()
    time.Sleep(1 * time.Second)  // "Ну, за секунду точно успеет"
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ключевая проблема &lt;code&gt;time.Sleep&lt;/code&gt; — это не ожидание выполнения, а случайная пауза, которая иногда совпадает с реальностью.&lt;/p&gt;&lt;p&gt;Почему под нагрузкой такой код даст сбой мгновенно? Это происходит из-за ряда причин: &lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;doWork()&lt;/code&gt; начинает тормозить&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;1 секунды перестаёт хватать&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;горутины накапливаются&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;программа завершается до завершения работы&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Формально код «рабочий». Но в реальности — это просто отложенный баг.&lt;/strong&gt;&lt;/p&gt;&lt;h4&gt;Правильное ожидание&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;func main() {
    done := make(chan struct{})
    go func() {
        defer close(done)
        doWork()
    }()
    &amp;lt;-done  // Ждём реального завершения
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;done := make(chan struct{})&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Это обычный канал, который используется как сигнал: «работа закончилась»&lt;/p&gt;&lt;p&gt;Тип &lt;code&gt;struct{}&lt;/code&gt; выбран не случайно — он ничего не занимает в памяти, а нам не нужно передавать данные, нам нужен сам факт.&lt;/p&gt;&lt;p&gt;Еще одна важная деталь — &lt;code&gt;defer close(done)&lt;/code&gt; как только &lt;code&gt;doWork()&lt;/code&gt; закончится (не важно — успешно или с ошибкой)  канал будет закрыт и это наш сигнал наружу об окончании.&lt;/p&gt;&lt;h4&gt;Как происходит ожидание&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;-done&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Здесь main-поток просто блокируется и ждёт и разблокируется он только в одном случае — когда канал закроют и это &lt;strong&gt;реальное&lt;/strong&gt; ожидание, а не «подождём секунду и надеемся».&lt;/p&gt;&lt;p&gt;Мы больше не гадаем:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt; хватит ли времени &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt; успеет ли задача &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt; не зависла ли она &lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4&gt;Чем это лучше time.Sleep&lt;/h4&gt;&lt;p&gt;&lt;code&gt;Sleep&lt;/code&gt; — это всегда предположение: «я думаю, этого времени хватит», а канал — это гарантия: «я точно знаю, что работа завершилась»&lt;/p&gt;&lt;p&gt;Можно также реализовать с контекстом (если работа может занять слишком много времени):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    done := make(chan struct{})
    go func() {
        defer close(done)
        doWork()
    }()
    select {
    case &amp;lt;-done:
        fmt.Println("успели")
    case &amp;lt;-ctx.Done():
        fmt.Println("не успели, но не утекли")
    }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;И здесь мы задаем правило: готов ждать максимум 5 секунд, тут очень важный момент без него система думает, что может ждать бесконечно&lt;/p&gt;&lt;pre&gt;&lt;code&gt;ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;То есть теперь наша логика следующая: или закончилась работа или вышло время, программа завершит работу.&lt;/p&gt;&lt;h3&gt;Инцидент второй: нагрузка выросла в 2 раза — латенси улетела&lt;/h3&gt;&lt;p&gt;Симптомы, знакомые многим, сервис наш работает корректно, ошибок нет, падений сервиса тоже нет, однако у нас растет время ответа сервиса &lt;code&gt;50ms → 800–1000ms CPU: 90–100%&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Например такая схема, убивающая production:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func handleFanOut(req Request) {
    results := make(chan Result, len(req.SubTasks))
    for _, task := range req.SubTasks {
        go func(t Task) {          // Нет лимита! Каждый запрос плодит N горутин
            res := callExternalService(ctx,t)
            results &amp;lt;- res
        }(task)
    }
    for i := 0; i &amp;lt; len(req.SubTasks); i++ {
        &amp;lt;-results
    }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Переводя это в цифры:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;1 запрос → 10 горутин&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;100 запросов → 1000 горутин&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;1000 запросов → 10 000 горутин (работает… пока работает)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;2000 запросов → 20 000 горутин — &lt;strong&gt;всё падает&lt;/strong&gt;&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Причём падает не в момент, когда вы ожидаете, а когда планировщик говорит: «Ребята, я больше не могу.»&lt;/p&gt;&lt;h4&gt;Диагностика и как это увидеть&lt;/h4&gt;&lt;p&gt;Одним из вариантов запуск в консоли:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Посмотреть число горутин
curl http://localhost:6060/debug/pprof/goroutine?debug=1
# Или через go tool
go tool pprof http://localhost:6060/debug/pprof/goroutine
# След планировщика — мастхэв
GODEBUG=schedtrace=1000 ./your-service&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Вывод &lt;code&gt;schedtrace&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;SCHED 1005ms: gomaxprocs=8 idleprocs=0 threads=11 spinningthreads=1 idlethreads=0 runqueue=152 [3 2 1 0 0 0 0 0]&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Ключевое на что нужно обратить внимание на &lt;strong&gt;runqueue=152&lt;/strong&gt; — это значит, что 152 горутины ждут выполнения, то есть наш планировщик захлёбывается.&lt;/p&gt;&lt;h4&gt;Исправление&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;func handleFanOutControlled(ctx context.Context, req Request) error {
    sem := semaphore.NewWeighted(10) // максимум 10 одновременных вызовов
    var wg sync.WaitGroup
    for _, task := range req.SubTasks {
        wg.Add(1)
        go func(t Task) {
            defer wg.Done()
            if err := sem.Acquire(ctx, 1); err != nil {
                return
            }
            defer sem.Release(1)
            callExternalService(ctx,t)
        }(task)
    }
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()
    select {
    case &amp;lt;-done:
        return nil
    case &amp;lt;-ctx.Done():
        return ctx.Err()
    }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Мы начинаем контролировать &lt;strong&gt;параллелизм&lt;/strong&gt;, а не количество запросов. Наша функция берет один запрос, разбивает его на подзадачи (&lt;code&gt;SubTasks&lt;/code&gt;) и обрабатывает их параллельно, но тут есть важная оговорка: не более 10 задач и плюс есть нормальное завершение и таймаут через &lt;code&gt;context&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;sem := semaphore.NewWeighted(10)&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;В любой момент времени можно выполнить не более 10 задач остальные будут ждать.&lt;/p&gt;&lt;p&gt;Внутри горутины - это будет работать так: &lt;/p&gt;&lt;pre&gt;&lt;code&gt;if err := sem.Acquire(ctx, 1); err != nil {
    return
}
defer sem.Release(1)&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;Acquire&lt;/code&gt; — «можно ли мне начать работу?» &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt; если лимит достигнут → ждём &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt; если &lt;code&gt;context&lt;/code&gt; отменён → выходим &lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;После завершения&lt;code&gt;Release&lt;/code&gt; освобождает слот и следующая задача может начать выполняться. Мы не ограничиваем создание goroutines (они всё равно создаются), но ограничиваем реальный параллелизм выполнения. Это дешевле, чем бесконтрольный fan-out, но всё ещё может давать overhead при очень большом числе задач.&lt;/p&gt;&lt;p&gt;Ожидание задач проходит через классический механизм &lt;code&gt;var wg sync.WaitGroup  &lt;/code&gt; &lt;/p&gt;&lt;pre&gt;&lt;code&gt;wg.Add(1)
...
defer wg.Done()&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Стоит подсветить важный нюанс:&lt;code&gt; return ctx.Err() &lt;/code&gt;не останавливает автоматически горутины, они могут продолжать работать. Чтобы горутины реально останавливались, контекст должен прокидываться внутрь всех долгих операций (например: callExternalService(ctx, t)), иначе они продолжат выполняться даже после отмены.&lt;/p&gt;&lt;h3&gt;Почему этот код — «правильный»&lt;/h3&gt;&lt;p&gt;Потому что здесь есть всё, чего обычно не хватает:&lt;/p&gt;&lt;h4&gt;1. Ограничение&lt;/h4&gt;&lt;p&gt;Не больше 10 задач одновременно → система не захлёбывается&lt;/p&gt;&lt;h4&gt;2. Ожидание&lt;/h4&gt;&lt;p&gt;Мы реально знаем, когда всё закончилось&lt;/p&gt;&lt;h4&gt;3. Таймаут&lt;/h4&gt;&lt;p&gt;Мы не зависаем бесконечно&lt;/p&gt;&lt;h3&gt;Инцидент третий: скрытая утечка горутин&lt;/h3&gt;&lt;p&gt;Пожалуй самый коварный сценарий. Тесты зелёные, память не растет, но через пару недель прод падает. Ниже упрощенный пример реального бага:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func startWorker(jobs &amp;lt;-chan Job) {
    go func() {
        for job := range jobs {
            process(job)
        }
        // если канал не закроется — сюда никогда не попадём
    }()
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На первый взгляд, выглядит безопасно, однако тут есть проблема: если канал jobs никто не закроет, горутина будет висеть вечно и каждый рестарт или вызов startWorker добавит новую.&lt;/p&gt;&lt;p&gt;Через неделю у нас 20k горутин, стек по 2-8 КБ каждая — уже ~100-150 МБ.  Память растёт медленно и не бросается в глаза, потому что сами данные маленькие, но суммарно тысячи горутин начинают съедать десятки мегабайт, а вот планировщик Go начинает задыхаться. В pprof/goroutine мы увидим тысячи &lt;code&gt;chan receive&lt;/code&gt;.&lt;/p&gt;&lt;h4&gt;Правильное завершение&lt;/h4&gt;&lt;p&gt;Правильный вариант должен начинаться с простой мысли: у горутины должен быть жизненный цикл. Исправляется это довольно просто:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;type Worker struct {
    jobs   chan int
    done   chan struct{}
    wg     sync.WaitGroup
}
func (w *Worker) Stop() {
    close(w.jobs)   // &amp;lt;- ключевой момент
    &amp;lt;-w.done        // ждём, пока worker закончит
}
func (w *Worker) Start() {
    w.wg.Add(1)
    go func() {
        defer w.wg.Done()
        defer close(w.done)
        for job := range w.jobs { // закроется, когда канал закроют
            process(job)
        }
    }()
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Тут мы указываем явно два сигнала:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;type Worker struct {
    jobs   chan int
    done   chan struct{}
    wg     sync.WaitGroup
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;jobs&lt;/code&gt; — откуда приходят задачи &lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;done&lt;/code&gt; — сигнал, что воркер &lt;strong&gt;полностью завершился&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;code&gt;for job := range &lt;/code&gt;&lt;a href="http://w.jobs" rel="noopener noreferrer nofollow"&gt;&lt;code&gt;w.jobs&lt;/code&gt;&lt;/a&gt; — это нормальный паттерн, но он работает корректно только, если канал когда-нибудь закроют. Как только &lt;code&gt;jobs &lt;/code&gt;закрывается — цикл сам завершится после этого: вызывается &lt;code&gt;wg.Done()&lt;/code&gt; закрывается &lt;code&gt;done →&lt;/code&gt; внешний мир узнаёт: «воркер реально закончился»&lt;/p&gt;&lt;p&gt;Ну и конечно: &lt;code&gt;close(&lt;/code&gt;&lt;a href="http://w.jobs" rel="noopener noreferrer nofollow"&gt;&lt;code&gt;w.jobs&lt;/code&gt;&lt;/a&gt;&lt;code&gt;)&lt;/code&gt; — мы явно говорим: «новых задач больше не будет», воркер дочитывает всё, что уже было в канале и выходит из цикла, закрывает &lt;code&gt;done &lt;/code&gt;мы дожидаемся этого через &lt;code&gt;&amp;lt;-w.done&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Ну и конечно, перед тем как написать &lt;code&gt;go func()&lt;/code&gt; — нужн задать себе два вопроса: Кто и когда закроет её канал? и Что произойдёт, если этого не случится?&lt;/p&gt;&lt;p&gt;Если не знаем ответа — закладываем явный context с таймаутом или канал stop с select.&lt;/p&gt;&lt;h3&gt;Инцидент четвёртый: внешний сервис начал тормозить, и мы упали вместе с ним&lt;/h3&gt;&lt;p&gt;Код из разряда «работает годами, а потом бац»:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func fetchData(url string) (*Response, error) {
    resp, err := http.Get(url)  // А что, если 30 секунд?
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return parse(resp)
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Представим вполне обычную ситуацию: внешний сервис, к которому мы обращаемся, внезапно начинает подвисать, то есть вместо ответа 50мс получаем - 20-40 секунд. А следом за ним и у нас начинает деградировать сервис: наши запрос не падают, они просто висят.&lt;/p&gt;&lt;p&gt;Планировщик не падает, он честно пытается это разрулить, но в какой-то момент он начинает тратить больше времени на переключения, чем на работу. И мы получаем классическое состояние: сервис жив, но пользоваться им уже невозможно&lt;/p&gt;&lt;p&gt;Самое неприятное — это не выглядит как авария. Нет красных алертов «всё умерло». Есть тихая деградация, которая разъедает систему.&lt;/p&gt;&lt;h4&gt;И тут вступает в действие Железное правило&lt;/h4&gt;&lt;p&gt;Любой вызов внешнего сервиса должен иметь таймаут. Даже если он «локальный». Даже если «99.99% времени отвечает за 5 мс».&lt;/p&gt;&lt;pre&gt;&lt;code&gt;func fetchDataSafe(url string) (*Response, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    client := &amp;amp;http.Client{
        Timeout: 2 * time.Second,  // второй круг защиты
    }
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return parse(resp)
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Два таймаута — не паранойя - это production. И если &lt;code&gt;context &lt;/code&gt;ограничивает время жизни запроса, то &lt;code&gt;http.Client.Timeout&lt;/code&gt; страхует на уровне клиента. Если один слой не сработал — сработает второй.&lt;/p&gt;&lt;h3&gt;Как увидеть проблемы до того, как они увидят вас&lt;/h3&gt;&lt;h4&gt;1. Обязательные метрики&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;import "github.com/prometheus/client_golang/prometheus"
var (
    goroutines = prometheus.NewGaugeFunc(
        prometheus.GaugeOpts{Name: "go_goroutines_current"},
        func() float64 { return float64(runtime.NumGoroutine()) },
    )
    // И это обязательно
    scheduleLatency = prometheus.NewHistogram(...)
)&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; — это вообще одна из самых недооценённых метрик.  В нормальной системе она ведёт себя примерно так: под нагрузкой выросла потом, вернулась обратно, но если она растёт и не падает, то это почти всегда сигнал, что что-то пошло не так.&lt;/p&gt;&lt;p&gt;&lt;code&gt;scheduleLatency&lt;/code&gt; — ещё более продвинутая штука, если она начинает расти, то планировщик уже не справляется.&lt;/p&gt;&lt;h4&gt;2. Профилирование в тестах&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;func TestNoGoroutineLeak(t *testing.T) {
    before := runtime.NumGoroutine()
    // ваш код
    time.Sleep(time.Second) // даём завершиться
    after := runtime.NumGoroutine()
    if after &amp;gt; before {
        t.Errorf("утечка: было %d, стало %d", before, after)
    }
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Да, это не идеальный тест, это база и он может давать шум, но как ранний сигнал — работает отлично.&lt;/p&gt;&lt;p&gt;Идея простая: замерили количество горутин «до», затем прогнали сценарий и дали системе чуть времени всё закрыть и конечно, посмотрели, что осталось&lt;/p&gt;&lt;h4&gt;3. pprof в каждом сервисе&lt;/h4&gt;&lt;p&gt;Тут можно сказать, что  пока всё хорошо — он не нужен. Когда становится плохо — без него почти невозможно понять, что происходит.  &lt;/p&gt;&lt;pre&gt;&lt;code&gt;import _ "net/http/pprof"
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;На практике открываем &lt;code&gt;/debug/pprof/goroutine&lt;/code&gt; и видим, где реально зависли горутины, так нам за 2 минуты понятна причина, вместо того чтобы часами гадать.&lt;/p&gt;&lt;p&gt;Помните: горутина — не бесплатная абстракция, это задача, которая конкурирует за ограниченный ресурс &lt;strong&gt;время планировщика&lt;/strong&gt;. И в следующий раз, когда увидите код без контекста, лимитов и таймаутов — знайте: это не баг. Это будущий инцидент, которого можно избежать уже сегодня.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">iik_812</dc:creator><pubDate>Mon, 27 Apr 2026 05:27:53 +0000</pubDate><guid>https://habr.com/ru/articles/1028264/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1028264</guid><category>изучение языков</category><category>go</category><category>golang</category><category>goroutine</category><category>goroutines</category><category>программирование</category><category>производительность</category><category>мониторинг</category><category>системное программирование</category><category>архитектура</category></item><item><title>gerpo: repository pattern для Go через указатели, без struct tags и кодогенерации</title><link>https://habr.com/ru/articles/1028320/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1028320</link><description>&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;time datetime="2026-04-27T05:15:20.000Z" title="2026-04-27, 05:15"&gt;16 минут назад&lt;/time&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4 мин&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div lang="ru"&gt;&lt;div id="post-content-body"&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Я пять лет писал на .NET, и там у меня сложилась привычка держать доменную модель отдельно от инфраструктуры хранения. Repository pattern — не как догма из книги Фаулера, а как рабочий способ не тащить &lt;code&gt;DbContext&lt;/code&gt;, маппинги и названия колонок в сущности. Домен остаётся доменом. Когда я перешёл на Go, меня сразу царапнули struct tags. Большинство библиотек работы с БД ожидает примерно такое:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;type User struct {
    ID    uuid.UUID `db:"id"`
    Email string    `db:"email"`
    Age   int       `db:"age"`
}&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Доменная сущность перестаёт быть доменной — она знает про свою схему в БД. Мне хотелось, чтобы сущность не знала вообще ничего, а всё знание жило в одном месте — в конфигурации репозитория, да-да, чистый перфекционизм. Конечно это не вся моя мотивация - ещё привычка и ,конечно же, лень. Для проектов среднего масштаба, хочется иметь универсальный репозиторий с кучей фильтров и пр., а не писать каждый метод для этого отдельно, даже если на них нет индексов, то в небольших проектах, пока это работает сносно, можно использовать и так, а потом уже добавить индексы.&lt;/p&gt;&lt;p&gt;&lt;br&gt;Готового решения под такую постановку я не нашёл. Поэтому написал своё — &lt;a href="https://github.com/Insei/gerpo" rel="noopener noreferrer nofollow"&gt;gerpo&lt;/a&gt;. Это проект из моих потребностей и вкусов. Я не претендую, что он лучше существующих инструментов, я просто его сделал для себя и решил им поделится, 20 апреля 2026 года вышла версия 1.0.0, и я решил, что пора рассказать про неё публично. Я разрабатывал её довольно долго, с 2024 года - редко и потихоньку, но нашел силы довести до v1.0.0.&lt;/p&gt;&lt;h2&gt;Парочка примеров, как это выглядит&lt;/h2&gt;&lt;p&gt;Конфигурация репозитория под модель User.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;type User struct {
    ID        uuid.UUID
    Email     *string
    Age       int
    CreatedAt time.Time
}
repo, err := gerpo.New[User]().
    Adapter(pgx5.NewPoolAdapter(pool)).
    Table("users").
    Columns(func(m *User, c *gerpo.ColumnBuilder[User]) {
        c.Field(&amp;amp;m.ID).OmitOnUpdate()
        c.Field(&amp;amp;m.Email)
        c.Field(&amp;amp;m.Age)
        c.Field(&amp;amp;m.CreatedAt).OmitOnUpdate()
    }).
    Build()&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Никаких тегов в структуре. Никакого &lt;code&gt;go generate&lt;/code&gt;. Модель остаётся чистой: четыре поля, четыре типа, ничего лишнего. Всё знание про то, как &lt;code&gt;User&lt;/code&gt; кладётся в таблицу &lt;code&gt;users&lt;/code&gt;, лежит в одном &lt;code&gt;Columns(…)&lt;/code&gt;-блоке.&lt;/p&gt;&lt;p&gt;Запрос на выборку:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;adults, _ := repo.GetList(ctx, func(m *User, h query.GetListHelper[User]) {
    h.Where().Field(&amp;amp;m.Age).GTE(18)
    h.OrderBy().Field(&amp;amp;m.CreatedAt).DESC()
    h.Page(1).Size(20)
})&lt;/code&gt;&lt;div&gt;&lt;a href="https://sourcecraft.dev/" rel="nofollow"&gt;&lt;img&gt;&lt;/a&gt;&lt;/div&gt;&lt;/pre&gt;&lt;p&gt;Снова те же &lt;code&gt;&amp;amp;m.Age&lt;/code&gt;, &lt;code&gt;&amp;amp;m.CreatedAt&lt;/code&gt;. Это не переменные и не лямбда-выражения из C#; это буквальные указатели на поля структуры, полученные через &lt;code&gt;&amp;amp;&lt;/code&gt;. Вся магия в том, что gerpo умеет сопоставить такой указатель с колонкой, которую вы сконфигурировали при сборке репозитория.&lt;br&gt;P.S. Ну вы понимаете откуда и почему такой стиль и способ - корни видны :)&lt;/p&gt;&lt;h2&gt;Центральная идея — field pointers&lt;/h2&gt;&lt;p&gt;На этапе &lt;code&gt;Build()&lt;/code&gt; gerpo один раз обходит структуру &lt;code&gt;User&lt;/code&gt; и запоминает смещение каждого поля через &lt;code&gt;unsafe.Offsetof&lt;/code&gt;. Когда потом в запросе приходит &lt;code&gt;&amp;amp;m.Age&lt;/code&gt;, gerpo смотрит на смещение и находит ту самую колонку, которую вы привязали к полю в &lt;code&gt;Columns(…)&lt;/code&gt;. Это не reflection на каждый запрос — на горячем пути идёт только указательная арифметика.Я специально не буду в этой статье копаться в том, как устроен внутренний модуль &lt;code&gt;fmap&lt;/code&gt;, как работает &lt;code&gt;unsafe.Offsetof&lt;/code&gt; на разных Go-версиях, как обеспечена безопасность при pointer receiver. Это отдельная история, и она потянет на вторую статью. Здесь важно одно: &lt;strong&gt;указатель на поле — первоклассный объект в API gerpo&lt;/strong&gt;. Им идентифицируют колонку в &lt;code&gt;Columns&lt;/code&gt;, в &lt;code&gt;Where&lt;/code&gt;, в &lt;code&gt;OrderBy&lt;/code&gt;, в &lt;code&gt;Only&lt;/code&gt;/&lt;code&gt;Exclude&lt;/code&gt;. Один и тот же лексический элемент — &lt;code&gt;&amp;amp;m.X&lt;/code&gt; — везде означает одно и то же. Нет переключения между "строковым именем колонки" и "именем поля" — это теперь одно целое.&lt;/p&gt;&lt;h2&gt;Что это даёт на практике&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Компилятор ловит удаление поля.&lt;/strong&gt; Если я уберу &lt;code&gt;Age&lt;/code&gt; из &lt;code&gt;User&lt;/code&gt;, все строки с &lt;code&gt;&amp;amp;m.Age&lt;/code&gt; перестанут компилироваться. Для struct-tag-библиотеки это ошибка рантайма, вы должны пройти все места sql генерации у удалить эту колонку и там, и если что-то забыто - всё просто перестанет работать где-то после деплоя.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Линтер ловит смену типа. &lt;/strong&gt;Если я поменяю &lt;code&gt;Age int&lt;/code&gt; на &lt;code&gt;Age string&lt;/code&gt;, тип указателя &lt;code&gt;&amp;amp;m.Age&lt;/code&gt; меняется. Дальше помогает &lt;code&gt;gerpolint&lt;/code&gt; — статический анализатор, который я написал специально для этого: он ловит &lt;code&gt;Field(&amp;amp;m.Age).EQ("18")&lt;/code&gt;, когда поле &lt;code&gt;int&lt;/code&gt;, и &lt;code&gt;Field(&amp;amp;m.Age).Contains("x")&lt;/code&gt;, когда &lt;code&gt;Contains&lt;/code&gt; применяется к нестроковому полю, и пр. кейсы, выдавая ожидаемые ошибки. Компилятор сам такой вызов пропустит, потому что &lt;code&gt;EQ&lt;/code&gt; принимает &lt;code&gt;any&lt;/code&gt;. Линтер — нет. Да это известное ограничение Generics в Go. Я решил обойти его тулингом линтера.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Нет тегов, нет кодогенерации.&lt;/strong&gt; Структура чистая, &lt;code&gt;go generate&lt;/code&gt; не нужен. IDE-рефакторинг переименования работает штатно, потому что &lt;code&gt;&amp;amp;m.Age&lt;/code&gt; — обычная Go-ссылка на поле.И здесь я обязан честно сказать про компромиссом с которым придется считаться, но и на этот условий тоже найдется фикс:&lt;br&gt;&lt;br&gt;&lt;strong&gt;Переименование поля меняет имя колонки.&lt;/strong&gt; По умолчанию gerpo берёт snake_case от имени Go-поля. Если вы переименуете &lt;code&gt;Age&lt;/code&gt; в &lt;code&gt;YearsOld&lt;/code&gt;, колонка в запросе станет &lt;code&gt;years_old&lt;/code&gt;. Это поведение convention over configuration: удобное, но ловушка, если вы переименовываете поле и миграция не переименовала колонку в БД. Компилятор здесь молчит.Для стабильности есть явное имя:&lt;br&gt;&lt;code&gt;c.Field(&amp;amp;m.Age).WithColumnName("age")&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Я держусь такого правила: для production-таблиц всегда ставлю &lt;code&gt;.WithColumnName(...)&lt;/code&gt;. В учебных примерах и в &lt;code&gt;examples/todo-api/&lt;/code&gt; полагаюсь на конвенцию — там переименование структуры и колонки должны идти синхронно, и это упрощает чтение. Выбор — ваш. Но тезис "рефакторинг полностью безопасен" — неправда, есть ложка дегтя в бочке с мёдом.&lt;/p&gt;&lt;h2&gt;Важные ограничения&lt;/h2&gt;&lt;p&gt;SQL, который gerpo на выходе генерирует, — PostgreSQL-шейп: плейсхолдеры &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;RETURNING&lt;/code&gt;, оконные функции, &lt;code&gt;CAST(? AS text)&lt;/code&gt; в LIKE. Версия 1.0 поддерживает только PostgreSQL (и PG-совместимые базы — CockroachDB и подобные — "drop-in", без формального тестирования). Multi-dialect — в бэклоге.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Инфраструктурный оверхед над чистым &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;pgx&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; На реальном PostgreSQL gerpo добавляет порядка 8% latency и около x2 аллокаций. В абсолютных числах — одна микросекунда на операцию, при запросе, который идёт 50–500 µс через сеть, это 0.2–2%. На горячих путях с сотнями тысяч RPS, где каждая аллокация критична, чистый &lt;code&gt;pgx&lt;/code&gt; будет предпочтительнее. В умеренных нагрузках разница в шуме.&lt;/p&gt;&lt;h2&gt;Что дальше&lt;/h2&gt;&lt;p&gt;Первая публичная версия gerpo стабильна, с документацией и runnable-примером. Дальше я планирую несколько статей написать, если будет интересно:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deep dive в &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;fmap&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Как на самом деле устроен механизм field pointers, почему &lt;code&gt;unsafe.Offsetof&lt;/code&gt; безопасен в том контексте, в котором gerpo его использует, что происходит при embedded-полях и указателях.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;&lt;strong&gt;gerpolint&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Статический анализатор для WHERE-фильтров уже доступен как отдельный бинарь и как плагин к &lt;code&gt;golangci-lint&lt;/code&gt; v2. Как он устроен, какие правила (GPL001–GPL006) реализует, как подключить в CI.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ссылки для тех, кто захочет пощупать:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/insei/gerpo" rel="noopener noreferrer nofollow"&gt;GitHub&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://insei.github.io/gerpo/" rel="noopener noreferrer nofollow"&gt;Документация&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://insei.github.io/gerpo/why-gerpo/" rel="noopener noreferrer nofollow"&gt;Сравнение с альтернативами&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/insei/gerpo/tree/main/examples/todo-api" rel="noopener noreferrer nofollow"&gt;Живой пример&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Если у вас есть вопросы, кейсы, где gerpo упадёт, или замечания к API — я читаю issues и благодарен за любой фидбек.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Insei</dc:creator><pubDate>Mon, 27 Apr 2026 05:15:20 +0000</pubDate><guid>https://habr.com/ru/articles/1028320/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1028320</guid><category>repository pattern</category><category>golang</category><category>go</category><category>postgresql</category><category>pgx</category><category>orm</category><category>generics</category><category>struct tags</category></item><item><title>Служишь Jira? Понятно</title><link>https://habr.com/ru/articles/1027716/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027716</link><description>&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;time datetime="2026-04-27T05:09:16.000Z" title="2026-04-27, 05:09"&gt;22 минуты назад&lt;/time&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2 мин&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div lang="ru"&gt;&lt;div id="post-content-body"&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;h2&gt;Вступление&lt;/h2&gt;&lt;p&gt;Замечаю, что часто люди подстраиваются под инструменты для работы, а не наоборот: процессы и поведение в целом деформируется под ограничения и логику треккеров и других методологических инструментов&lt;/p&gt;&lt;p&gt;Хотел бы в свободной форме порассуждать в статье о том, почему так происходит, и что с этим делать&lt;/p&gt;&lt;h2&gt;Что происходит?&lt;/h2&gt;&lt;p&gt;На уровне отдельно взятого человека происходят изменения в мышлении. Человек перестаёт думать "что именно нужно сделать", задача подгоняется под инстумент&lt;/p&gt;&lt;p&gt;Если инструмент делает что-то сложным — люди перестают это делать, даже если это важно. Если делает что-то лёгким — делают это чаще, даже если это не нужно&lt;/p&gt;&lt;p&gt;Привычка подменяет целесообразность. "Мы так делаем, потому что так работает наша система" — это становится достаточным обоснованием&lt;/p&gt;&lt;p&gt;На уровне команды тоже всё плывёт. Процессы формируются вокруг возможностей инструмента, а не вокруг реальных потребностей. Таск-треккер и его доска диктуют методологию, а не методология — структуру доски&lt;/p&gt;&lt;p&gt;Ритуалы обслуживания инструмента становятся самоцелью — обновление статусов, заполнение полей, ведение отчётов — работа ради работы&lt;/p&gt;&lt;p&gt;Метрики определяются не тем, что важно, а тем, что инструмент умеет считать. Измеримое побеждает значимое&lt;/p&gt;&lt;h2&gt;Почему так происходит?&lt;/h2&gt;&lt;p&gt;Адаптироваться к инструменту проще, чем настраивать инструмент под себя — это раз&lt;/p&gt;&lt;p&gt;Инструменты предлагают готовую модель мира — легче принять чужую модель, чем строить свою — это два&lt;/p&gt;&lt;p&gt;Когда команда адаптировалась — отклонение от "инструментальной нормы" воспринимается как отклонение от командной нормы — это три&lt;/p&gt;&lt;p&gt;На мой взгляд это основные, но не единственные причины&lt;/p&gt;&lt;h2&gt;Что с этим делать?&lt;/h2&gt;&lt;p&gt;Во-первых, признать проблему. Нужно её явно проговорить с командой. Без этого никуда&lt;/p&gt;&lt;p&gt;Во-вторых, легализовать обходные пути. Если люди систематически делают что-то "мимо" инструмента — это не нарушение дисциплины, а информация о том, что инструмент не отражает реальность. Разберитесь с причиной, а не боритесь с симптомами&lt;/p&gt;&lt;p&gt;В-третьих, не путать "работу в инструменте" с "работой". Двигать карточки по доске — не работа. Заполненный отчёт — это не результат сам по себе. Если команда тратит ощутимую часть времени на обслуживание инструмента — что-то пошло не так&lt;/p&gt;&lt;p&gt;И последнее — выработайте в команде принципы, которым все будут придерживаться. Инструменты меняются, практики адаптируются, принципы остаются. Если команда не может сформулировать свои принципы отдельно от названия инструмента (jira, youtrack etc) — она уже подчинена ему&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">octav47</dc:creator><pubDate>Mon, 27 Apr 2026 05:09:16 +0000</pubDate><guid>https://habr.com/ru/articles/1027716/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027716</guid><category>управление людьми</category><category>управление командой</category><category>управление разработкой</category><category>тимлид</category><category>тимлидство</category></item><item><title>Облава на инсайдеров с Polymarket, а также уход Тима Кука на почетную пенсию</title><link>https://habr.com/ru/articles/1027864/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027864</link><description>&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;time datetime="2026-04-27T05:01:07.000Z" title="2026-04-27, 05:01"&gt;30 минут назад&lt;/time&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;7 мин&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Дайджест&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div lang="ru"&gt;&lt;div id="post-content-body"&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Самые интересные новости финансов и технологий в России и мире за неделю: ЦБ снизил ставку до 14,5%, Швеция не верит в 6% инфляцию в РФ, очередное покушение на Трампа, xAI Илона Маска не умеет правильно использовать видюхи, а также массовые сокращения у Цукерберга. &lt;/p&gt;&lt;h3&gt;Тема недели: Инсайдеров на рынках предсказаний начали щемить&lt;/h3&gt;&lt;p&gt;🐌 На того самого &lt;a href="https://polymarket.com/0x31a56e9e690c621ed21de08cb559e9524cdb8ed9" rel="noopener noreferrer nofollow"&gt;чела&lt;/a&gt;, который заработал $400к на ставках про арест Мадуро, американская прокуратура &lt;a href="https://www.rbc.ru/rbcfreenews/69eab9659a79478a8aa33df5?from=main_lines_14" rel="noopener noreferrer nofollow"&gt;завела уголовку&lt;/a&gt;. Вопреки ожиданиям, это оказался не сын Трампа, а один из спецназовцев, принимавших участие в спецоперации – теперь ему может грозить 20 лет тюрьмы. Кстати чел очень глупо попался: зарегистрировал аккаунт на Полимаркете на свою электронную почту (я писал &lt;a href="https://t.me/RationalShitposting/660" rel="noopener noreferrer nofollow"&gt;вот здесь&lt;/a&gt;, почему так делать не стоит).&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/32d/0a2/45b/32d0a245bade2cce9f20b6831a7f23d5.jpg" alt="Фотка этого самого спецназовца-лудомана. Подробнее про всю эту ситуацию (и про то, почему вообще инсайдерская торговля запрещена) написал у себя на канале" title="Фотка этого самого спецназовца-лудомана. Подробнее про всю эту ситуацию (и про то, почему вообще инсайдерская торговля запрещена) написал у себя на канале" width="640" height="640"&gt;&lt;div&gt;&lt;figcaption&gt;Фотка этого самого спецназовца-лудомана. Подробнее про всю эту ситуацию (и про то, почему вообще инсайдерская торговля запрещена) написал &lt;a href="https://t.me/RationalAnswer/1632" rel="noopener noreferrer nofollow"&gt;у себя на канале&lt;/a&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;🐌 В Твиттере, тем временем, все угорают над тем, что по всем признакам главными любителями побаловаться инсайдами на фондовом рынке являются кореша и приближенные президента США – но показательно арестовали в итоге только какого-то низового солдатика. Хотя, справедливости ради, в Нью-Йорке на этой неделе сотрудникам правительства официально &lt;a href="https://www.governor.ny.gov/news/governor-hochul-signs-nation-leading-executive-order-banning-state-employees-insider-trading" rel="noopener noreferrer nofollow"&gt;запретили&lt;/a&gt; инсайдер-трейдить на рынках предсказаний.&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/833/96e/4bd/83396e4bd513f7fa8b50c9ba5ea29df2.png" width="507" height="590"&gt;&lt;/figure&gt;&lt;p&gt;🐌 Polymarket &lt;a href="https://www.theguardian.com/world/2026/apr/23/hairdryer-or-lighter-french-police-look-at-claim-of-sensor-tampering-to-win-weather-bets" rel="noopener noreferrer nofollow"&gt;прогрели&lt;/a&gt; на $34к. Один чувак сделал несколько крайне маловероятных ставок на максимальную температуру в самый жаркий день недели в Париже. А так как источником данных для этого пари является один-единственный датчик «Meteo France» в местном аэропорту, челу пришла гениальная идея каждый день просто разогревать датчик с помощью фена. Итог – ставочки зашли, «прогрев рынка» прошел успешно, а темщика теперь ищет полиция Франции.&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/0c6/66b/f07/0c666bf076ae059f751d7f294ff38ae8.jpg" alt="Как шутят в Твиттере, «похоже, невидимая рука рынка держит фен»" title="Как шутят в Твиттере, «похоже, невидимая рука рынка держит фен»" width="584" height="147"&gt;&lt;div&gt;&lt;figcaption&gt;Как шутят в Твиттере, «похоже, невидимая рука рынка держит фен»&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;🐌 Polymarket и Kalshi &lt;a href="https://forklog.com/news/kalshi-i-polymarket-zapustyat-bessrochnye-fyuchersy/" rel="noopener noreferrer nofollow"&gt;запустят&lt;/a&gt; бессрочные фьючерсы на курсы криптовалют. Polymarket заанонсил новый функционал прямо у себя в &lt;a href="https://x.com/Polymarket/status/2046653304810156283" rel="noopener noreferrer nofollow"&gt;Твиттере&lt;/a&gt; вместе с интерфейсом фьючерсов. Про старт такого же проекта у Kalshi &lt;a href="https://archive.ph/nKnEV" rel="noopener noreferrer nofollow"&gt;говорят&lt;/a&gt; инсайдеры The Information. Это жирнейший кусок рынка, интересно – получится ли у этих площадок отжать долю у традиционных игроков, типа Hyperliquid?&lt;/p&gt;&lt;h3&gt;Россия: Споры вокруг инфляции&lt;/h3&gt;&lt;p&gt;🐌 ЦБ ожидаемо &lt;a href="https://www.cbr.ru/press/pr/?file=24042026_133000key.htm" rel="noopener noreferrer nofollow"&gt;снизил&lt;/a&gt; ставку на полпункта до 14,5% – это уже восьмое снижение подряд с июня 2025 года. В пресс-релизе Центробанка &lt;a href="https://www.cbr.ru/press/pr/?file=24042026_133000key.htm" rel="noopener noreferrer nofollow"&gt;раздали &lt;/a&gt;немного аналитики по состоянию российской экономики: в падении ВВП в начале года в том числе виновато повышение налогов, а также пишут о риске ускорения роста цен из-за войны на Ближнем Востоке, роста зарплат быстрее производительности труда, и высоких расходов при дефиците бюджета.&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/e6a/7b8/d55/e6a7b8d55cc122ff621365bd2c50a4db.jpg" alt="Это цитата из официальной прессухи, если что. Эльвира Сахипзадовна, вы такими тейками народ по поводу победы над инфляцией не сильно успокаиваете, если честно!!" title="Это цитата из официальной прессухи, если что. Эльвира Сахипзадовна, вы такими тейками народ по поводу победы над инфляцией не сильно успокаиваете, если честно!!" width="1478" height="1064"&gt;&lt;div&gt;&lt;figcaption&gt;Это цитата из официальной прессухи, если что. Эльвира Сахипзадовна, вы такими тейками народ по поводу победы над инфляцией не сильно успокаиваете, если честно!!&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;🐌 Глубокая аналитика подъехала откуда не ждали: разведка Швеции заинтересовалась инфляцией в России. Глава ведомства &lt;a href="https://archive.ph/KPxIW" rel="noopener noreferrer nofollow"&gt;заявил&lt;/a&gt;, дескать, реальная инфляция в РФ на самом деле болтается где-то под 15% годовых (близко к ключевой ставке), а наш ЦБ искусственно улучшает стату, заявляя лишь о 6%. Все конспирологи из секты «заниженной в три раза инфляции» (это, кстати, не только российская тема – таких в каждой стране полно) ликуют, но со шведскими тезисами согласны &lt;a href="https://thebell.site/evropeyskaya-razvedka-obvinila-rossiyu-v-falsifikatsii-statistiki-pravda-li-chto-tsifry-vrut" rel="noopener noreferrer nofollow"&gt;далеко не все&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;🐌 Рубрика «Новые налоги и сборы»: на этот раз под раздачу попали продавцы электроники. Минпромторг &lt;a href="https://www.vedomosti.ru/economics/news/2026/04/21/1191866-minpromtorg-ustanovil-tehsbor" rel="noopener noreferrer nofollow"&gt;определил&lt;/a&gt; размер технологического сбора с иностранных смартфонов и ноутбуков на уровне 250 и 500 руб. Всё во имя поддержки отечественного производителя. &lt;/p&gt;&lt;p&gt;🐌 Ну и наконец: Евросоюз релизнул &lt;a href="https://www.vedomosti.ru/politics/news/2026/04/23/1192571-20-ii-paket-i-kredit" rel="noopener noreferrer nofollow"&gt;новый пакет санкций&lt;/a&gt; против России. Из интересного: там почикали Челябинвестбанк – который был &lt;a href="https://t.me/RationalShitposting/531" rel="noopener noreferrer nofollow"&gt;известен тем,&lt;/a&gt; что оставался одним из немногих российских банков, позволяющих нормально принимать валюту из Interactive Brokers. А ребята из OhMySwift еще &lt;a href="https://t.me/ohmyswiftblog/947" rel="noopener noreferrer nofollow"&gt;добавляют,&lt;/a&gt; что для жителей Евросоюза использование белорусских криптобирж теперь забанено. Забавно, что эту самую «Цифру» (которая была одним из основных каналов беспроблемного вывода рублей из РФ через крипту) практически одновременно запрещают в ЕС, и собираются прикрыть с другой стороны тоже – в новом российском крипторегулировании.&lt;/p&gt;&lt;h3&gt;Новое покушение на Трампа&lt;/h3&gt;&lt;p&gt;🐌 Дональд Трамп пришел на ужин с корреспондентами Белого Дома, и туда же &lt;a href="https://www.gazeta.ru/politics/2026/04/26/22844209.shtml" rel="noopener noreferrer nofollow"&gt;пытался прорваться&lt;/a&gt; чел с дробовиками, пистолетами и ножами (правда, безуспешно – его задержали еще на подступах). Нападавшим &lt;a href="https://apnews.com/article/trump-correspondents-dinner-shooter-cole-tomas-allen-ea98b14e839217985bd7cf5ab169fb65" rel="noopener noreferrer nofollow"&gt;оказался&lt;/a&gt; Коул Томас Аллен, 31-летний школьный учитель из Калифорнии. В прошлый раз перед выборами, кстати, на Трампа покушался чувак по имени Томас Крукс. Что все Томасы имеют против Трампа??&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/e97/471/b2a/e97471b2ae85f0f5aa1c7f0b0fda7c99.jpg" alt="К слову, у чувака была награда «учитель месяца». Иногда складывается впечатление, что все новости в США делятся на два типа: «ученик школы пришел куда-то со стволом» и «школьный учитель пришел куда-то со стволом»" title="К слову, у чувака была награда «учитель месяца». Иногда складывается впечатление, что все новости в США делятся на два типа: «ученик школы пришел куда-то со стволом» и «школьный учитель пришел куда-то со стволом»" width="901" height="976"&gt;&lt;div&gt;&lt;figcaption&gt;К слову, у чувака была награда «учитель месяца». Иногда складывается впечатление, что все новости в США делятся на два типа: «ученик школы пришел куда-то со стволом» и «школьный учитель пришел куда-то со стволом»&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Чё там в Иране&lt;/h3&gt;&lt;p&gt;🐌 Продолжаем мониторить ситуацию на Ближнем Востоке: Дональд Трамп в одностороннем порядке &lt;a href="https://www.rbc.ru/politics/21/04/2026/69e7da269a7947147b6701d8" rel="noopener noreferrer nofollow"&gt;объявил&lt;/a&gt; о продолжении перемирия с Ираном на неопределенный срок, но вообще – никакой долгосрочной сделки на горизонте пока не видно. По сути, иранская сторона особо не горит желанием даже пытаться вести переговоры в том формате, которого хочется Трампу.&lt;/p&gt;&lt;p&gt;🐌 По самому Ормузскому проливу всё стабильно: Иран &lt;a href="https://www.rbc.ru/politics/22/04/2026/69e8deb29a79476cd05a8010" rel="noopener noreferrer nofollow"&gt;продолжает&lt;/a&gt; обстреливать танкеры, а Штаты &lt;a href="https://www.rbc.ru/rbcfreenews/69e948929a79473767286099?from=newsfeed" rel="noopener noreferrer nofollow"&gt;поддерживать&lt;/a&gt; ответную блокаду против иранских и заплативших пошлину Ирану кораблей. Тем временем, всей ситуацией &lt;a href="https://www.reuters.com/world/middle-east/scam-messages-offering-ships-safe-transit-through-hormuz-security-firm-warns-2026-04-21/" rel="noopener noreferrer nofollow"&gt;вдохновились мошенники&lt;/a&gt;: неизвестные под видом иранской стороны предлагают судоходным компаниям заплатить в крипте «за безопасный проход через пролив», а потом таких бедолаг обстреливают на локации уже настоящие иранцы.&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/87d/009/8ed/87d0098ed2737f32fdfde2d85da74361.jpg" width="1254" height="1254"&gt;&lt;/figure&gt;&lt;p&gt;🐌 Кстати, Tether тут на днях забанил пару кошельков на Троне с рекордными $344 млн в USDT. И чуть позже США официально &lt;a href="https://www.thestreet.com/crypto/markets/treasury-secretary-unveils-economic-fury-to-freeze-344m" rel="noopener noreferrer nofollow"&gt;заявили&lt;/a&gt;, что это на самом деле такая спецоперация Economic Fury, а кошельки принадлежали иранскому режиму. У меня один вопрос: иранцы, вы чё, серьезно в этой ситуации юзаете централизованные стейблкоины, а не шифропанковский биткоин? Вы там вообще нормальные? 🤔&lt;/p&gt;&lt;p&gt;🐌 И еще на этой неделе &lt;a href="https://www.rbc.ru/politics/20/04/2026/69e551479a794797af8d57f3" rel="noopener noreferrer nofollow"&gt;WSJ сообщила&lt;/a&gt; об экономических переговорах между США и ОАЭ. Эмираты сильно недовольны всей движухой в регионе и требуют от Штатов хотя бы финансовой подстраховки (в виде открытой линии предоставления долларовой ликвидности) на случай, если всё это дело затянется и у арабов начнется дефицит баксов. В случае провала переговоров, намекают, что могут в кризисной ситуации даже начать торговать нефтью за юани.&lt;/p&gt;&lt;h3&gt;Тим Кук покидает свою джобс&lt;/h3&gt;&lt;p&gt;🐌 В понедельник &lt;a href="https://www.apple.com/newsroom/2026/04/tim-cook-to-become-apple-executive-chairman-john-ternus-to-become-apple-ceo/" rel="noopener noreferrer nofollow"&gt;Apple объявили&lt;/a&gt;, что 1 сентября этого года Тим Кук перейдёт с поста главы корпорации на должность исполнительного председателя совета директоров, где будет заниматься международными связями. Интересный факт: за период нахождения в должности CEO (2011–2026) Тим Кук смог &lt;a href="https://www.theguardian.com/technology/2026/apr/20/tim-cook-apple-steve-jobs" rel="noopener noreferrer nofollow"&gt;увеличить капитализацию&lt;/a&gt; компании с $0,35 трлн до нынешних $4,0 трлн. &lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/df6/289/392/df628939227eeea0329d5730c9a1fd99.jpg" width="1254" height="1254"&gt;&lt;/figure&gt;&lt;p&gt;Кстати, недавно Bloomberg &lt;a href="https://www.bloomberg.com/news/articles/2026-04-22/tim-cook-regrets-maps-flub-sees-apple-watch-as-his-proudest-work?sref=l3J6d079" rel="noopener noreferrer nofollow"&gt;слили&lt;/a&gt; внутренний созвон Apple, где Тим Кук назвал свои главные ошибки на посту CEO: Apple Car, &lt;a href="https://en.wikipedia.org/wiki/AirPower_(Apple)" rel="noopener noreferrer nofollow"&gt;AirPower&lt;/a&gt; (зарядный коврик для айфона), а также Apple Maps. По картам прошелся особенно жестко: продукт был сырой и тестился чуть ли не на одних сотрудниках и на окрестностях Купертино.&lt;/p&gt;&lt;p&gt;Новым CEO станет 50-летний &lt;a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%80%D0%BD%D1%83%D1%81,_%D0%94%D0%B6%D0%BE%D0%BD" rel="noopener noreferrer nofollow"&gt;Джон Тернус&lt;/a&gt;. В Apple он занимался разработкой всяких железок: ИИ-очками, умным кулоном с камерой, домашней робототехникой и прочее прочее. Чуть подробнее про него см. в канале &lt;a href="https://t.me/niketasfm/2382" rel="noopener noreferrer nofollow"&gt;«Радиорубка Лихачёва».&lt;/a&gt;&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/a06/b13/04f/a06b1304f90fbc30dd7715d88ac0e216.png" alt="LinkedIn-профиль Тернуса. Это что же, получается, тезис «чтобы найти хорошую работу в IT, важно иметь правильно оформленный и прокачанный профиль на LinkedIn» всё это время был одной большой ложью??" title="LinkedIn-профиль Тернуса. Это что же, получается, тезис «чтобы найти хорошую работу в IT, важно иметь правильно оформленный и прокачанный профиль на LinkedIn» всё это время был одной большой ложью??" width="1180" height="1280"&gt;&lt;div&gt;&lt;figcaption&gt;LinkedIn-профиль Тернуса. Это что же, получается, тезис «чтобы найти хорошую работу в IT, важно иметь правильно оформленный и прокачанный профиль на LinkedIn» всё это время был одной большой ложью??&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;h3&gt;Новости AI: Нейропроблемы Маска&lt;/h3&gt;&lt;p&gt;🐌 Весной &lt;a href="https://www.businessinsider.com/elon-musk-xai-compute-cursor-ai-model-training-2026-4" rel="noopener noreferrer nofollow"&gt;выяснилось&lt;/a&gt;, что показатель утилизации мощностей GPU при обучении моделей у xAI Илона Маска составляет всего 11%, а у конкурентов он находится на уровне 35–45%. Это значит, что на обучение модели Икс приходится тратить в 3 раза больше времени при прочих равных. Дошло до того, что они свои мощности начали &lt;a href="https://archive.ph/1QtEE" rel="noopener noreferrer nofollow"&gt;сдавать &lt;/a&gt;в аренду чувакам из Cursor, которые используют их эффективнее. &lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/868/620/225/8686202254a2ea5b2d33495134d8d250.jpeg" alt="Твое лицо, когда накупил дорогущих видеокарт, а рукожопы-инженеры используют их всего на 11%" title="Твое лицо, когда накупил дорогущих видеокарт, а рукожопы-инженеры используют их всего на 11%" width="1080" height="1054"&gt;&lt;div&gt;&lt;figcaption&gt;Твое лицо, когда накупил дорогущих видеокарт, а рукожопы-инженеры используют их всего на 11%&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Так вот, продолжение такое: SpaceX, в которую теперь входит xAI, &lt;a href="https://archive.ph/W2HZf" rel="noopener noreferrer nofollow"&gt;заключила &lt;/a&gt;сделку о праве приобретения Cursor за некислые $60 млрд. Сейчас компании вместе работают над эффективной нейронкой для вайб-кодинга. Если Маска устроит результат, то до конца года покупку завершат, а если решат отказаться от слияния, то придется за выход из сделки заплатить штрафные $10 млрд.&lt;/p&gt;&lt;p&gt;🐌 OpenAI &lt;a href="https://openai.com/index/introducing-chatgpt-images-2-0/" rel="noopener noreferrer nofollow"&gt;выпустили&lt;/a&gt; ChatGPT Images 2.0. Улучшенную версию того самого генератора, который был виновен в гиблификации мирового инета. OpenAI нейронку сейчас рекламят, по большей части, через то, что модель научилась исправно ладить с генерацией текста на картинках (причем не только на английском), а также еще точнее следовать инструкциям. Возможно, эпоха всратых мемных нейроазбук на разные темы уйдет в прошлое – т.к. современные модели уже способны нагенерить что-то внятное.&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/709/2a2/d83/7092a2d83241a23b2dee4850f65f6d9e.jpg" alt="Попросил вот ЧатГПТ сделать мне азбуку финансистов – даже никакой очевидной лажи навскидку не видно (ну, разве что, вместо Когана на картинку какой-то Гуриев прокрался)" title="Попросил вот ЧатГПТ сделать мне азбуку финансистов – даже никакой очевидной лажи навскидку не видно (ну, разве что, вместо Когана на картинку какой-то Гуриев прокрался)" width="708" height="1000"&gt;&lt;div&gt;&lt;figcaption&gt;Попросил вот ЧатГПТ сделать мне азбуку финансистов – даже никакой очевидной лажи навскидку не видно (ну, разве что, вместо Когана на картинку какой-то Гуриев прокрался)&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;🐌 Anthropic &lt;a href="https://www.anthropic.com/engineering/april-23-postmortem" rel="noopener noreferrer nofollow"&gt;объяснила&lt;/a&gt;, почему у Claude Code в последнее время просело качество кода. Оказалось, что внутри случайно поменяли настройки «глубины размышления», еще был баг, который удалял часть размышлений модели в старых сессиях, а также разрабы попросили нейронку писать короче – из-за этого она стала хуже соображать. Вместе с выходом отчёта пользователям сбросили лимиты и пообещали больше так не лажать (в целом – похвально, что они вообще такой подробный пост-мортем разбор опубликовали).&lt;/p&gt;&lt;p&gt;🐌 Экстремистская Meta &lt;a href="https://frankmedia.ru/275879" rel="noopener noreferrer nofollow"&gt;сокращает&lt;/a&gt; 10% своих работяг, затронет примерно 8 тысяч человек. Причина простая: корпорация тратит очень много денег на ИИ, надо теперь где-то экономить! Кстати, помимо Цукерберга пытаться массово избавляться от сотрудников на этой неделе стала Microsoft: она &lt;a href="https://www.bloomberg.com/news/articles/2026-04-23/microsoft-offers-voluntary-retirement-to-about-7-of-us-workers" rel="noopener noreferrer nofollow"&gt;направила&lt;/a&gt; нескольким тысячам работников письма с просьбой выйти на пенсию досрочно.&lt;/p&gt;&lt;h3&gt;Новости крипты: Взлом Kelp/Aave&lt;/h3&gt;&lt;p&gt;🐌 Еще в конце позапрошлой недели произошел крупный хак в DeFi-сообществе на ~$200 млн –  была затронута в том числе крупнейшая платформа для DeFi-лендинга под названием Aave. Подробнее про всю эту историю я &lt;a href="https://habr.com/ru/articles/1026416/" rel="noopener noreferrer nofollow"&gt;написал&lt;/a&gt; у себя на канале, а сейчас давайте про то, чем всё завершилось: участники эко-системы DeFi &lt;a href="https://forklog.com/news/defi-protokoly-sobrali-desyatki-tysyach-eth-dlya-vosstanovleniya-aave" rel="noopener noreferrer nofollow"&gt;решили скинуться &lt;/a&gt;на покрытие образовавшейся дырки, сейчас она уже &lt;a href="https://x.com/tomwanhh/status/2048339892065693943" rel="noopener noreferrer nofollow"&gt;закрыта&lt;/a&gt; практически полностью (но осадочек остался, ну и все голосования участников протоколов должны состояться еще).&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/ae8/5fc/b20/ae85fcb207f240d1d4b199665d5c9a5e.png" alt="Там кажется скинулись практически все, кто мог, – но немного иронично, что взносы от проштрафившихся пацанов из KelpDAO / Layer Zero (которых изначально и ломанули) не самые крупные" title="Там кажется скинулись практически все, кто мог, – но немного иронично, что взносы от проштрафившихся пацанов из KelpDAO / Layer Zero (которых изначально и ломанули) не самые крупные" width="806" height="732"&gt;&lt;div&gt;&lt;figcaption&gt;Там кажется скинулись практически все, кто мог, – но немного иронично, что взносы от проштрафившихся пацанов из KelpDAO / Layer Zero (которых изначально и ломанули) не самые крупные&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;🐌 Журнал Le Monde &lt;a href="https://www.lemonde.fr/societe/article/2026/04/24/enlevements-dans-le-secteur-des-cryptomonnaies-88-mises-en-examen-dans-les-enquetes-en-cours-selon-le-pnaco_6683108_3224.html" rel="noopener noreferrer nofollow"&gt;выпустил&lt;/a&gt; статью с интересной статистикой: в настоящий момент во Франции предъявлены обвинения 88 людям (из них 10 несовершеннолетних) по делам, связанным с похищениями криптанов ради выкупа; а всего таких инцидентов за последние три года было больше 130. &lt;a href="https://t.me/RationalAnswer/1633" rel="noopener noreferrer nofollow"&gt;Написал&lt;/a&gt; у себя на канале про то, как всё это связано с грядущим созданием «реестра криптанов» в РФ (который должен начать действовать с июля 2026-го).&lt;/p&gt;&lt;h3&gt;Интервью недели: Лудоман здорового человека&lt;/h3&gt;&lt;p&gt;В этой рубрике я обычно рассказываю об одном подкасте, который я послушал на прошлой неделе. На этот раз это &lt;a href="https://www.youtube.com/watch?v=5qkzmhIfl0o" rel="noopener noreferrer nofollow"&gt;свежее интервью&lt;/a&gt; успешного (анонимного) Polymarket-трейдера &lt;a href="https://x.com/r_gopfan" rel="noopener noreferrer nofollow"&gt;gopfan2&lt;/a&gt; на канале Евгения Фельдмана. Это на самом деле математик из России – он рассказывает про особенности рынка предсказаний и про то, как он там заработал больше $1,5 млн за последние несколько лет. Завтра еще выложу у себя в ТГ разбор самых интересных моментов из интервью.&lt;/p&gt;&lt;h3&gt;Хорошая новость недели&lt;/h3&gt;&lt;p&gt;В Японии &lt;a href="https://www.instagram.com/reel/DXcIgo1iNkR/" rel="noopener noreferrer nofollow"&gt;начался сезон&lt;/a&gt; цветения голубой немофилы. Красиво!&lt;/p&gt;&lt;figure&gt;&lt;img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/6de/e26/55d/6dee2655da78d7491f54c3c7ec78acdd.jpg" width="650" height="433"&gt;&lt;/figure&gt;&lt;h3&gt;Бонусные посты недели из моих ТГ-каналов:&lt;/h3&gt;&lt;p&gt;🐌 Взлом Kelp/Aave, или &lt;a href="https://t.me/RationalAnswer/1628" rel="noopener noreferrer nofollow"&gt;почему&lt;/a&gt; я не инвестирую в DeFi.&lt;/p&gt;&lt;p&gt;🐌 Чем &lt;a href="https://t.me/RationalAnswer/1630" rel="noopener noreferrer nofollow"&gt;рынки предсказаний лучше&lt;/a&gt; фондового рынка (я там уже перевалил за $2000+ прибыли).&lt;/p&gt;&lt;p&gt;🐌 &lt;a href="https://t.me/RationalAnswer/1633" rel="noopener noreferrer nofollow"&gt;Разбор &lt;/a&gt;тревожной ситуации с криптанами во Франции (и что в этой связи ожидать в скором будущем в России).&lt;/p&gt;&lt;p&gt;🐌 Как &lt;a href="https://t.me/RationalAnswer/1632" rel="noopener noreferrer nofollow"&gt;работают&lt;/a&gt; законы США против инсайдерской торговли: там всё немного не так, как многие думают.&lt;/p&gt;&lt;p&gt;🐌 Мини-разбор &lt;a href="https://t.me/RationalAnswer/1634" rel="noopener noreferrer nofollow"&gt;реф-программы&lt;/a&gt; Interactive Brokers.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">RationalAnswer</dc:creator><pubDate>Mon, 27 Apr 2026 05:01:07 +0000</pubDate><guid>https://habr.com/ru/articles/1027864/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027864</guid><category>новости финансов</category><category>новости технологий</category><category>дайджест</category><category>новости недели</category></item><item><title>SDD на масштабе FullStack-приложения: 17 спринтов, две конституции, три чата</title><link>https://habr.com/ru/articles/1027886/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027886</link><description>&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;time datetime="2026-04-27T05:00:14.000Z" title="2026-04-27, 05:00"&gt;31 минуту назад&lt;/time&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;11 мин&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div lang="ru"&gt;&lt;div id="post-content-body"&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;В первой статье я писал про SDD за один вечер — Telegram-бот, шесть команд Spec Kit, восемь часов от первого &lt;code&gt;speckit.constitution&lt;/code&gt; до рабочего MVP. Это была проверка методологии на маленькой задаче.&lt;/p&gt;&lt;p&gt;С тех пор я прошёл 17 спринтов SDD на FullStack-приложении: B2C-трекер привычек и целей, два репозитория (backend и frontend), 251 тест на бэке и 77 на фронте, релиз в продакшен. Это уже не вечер — это полный цикл разработки FullStack-приложения по одной методологии.&lt;/p&gt;&lt;p&gt;Здесь — что не дало мне потерять контроль на этом масштабе. Не «как быстро я это сделал», а &lt;strong&gt;как методология держит управляемость&lt;/strong&gt;, когда фич много, репозиториев два, спринты идут параллельно и каждая фича касается обеих сторон.&lt;/p&gt;&lt;h3&gt;Что построено за 17 спринтов&lt;/h3&gt;&lt;p&gt;Чтобы дальше говорить о методологии — короткий контекст того, что под ней.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;LifeSync&lt;/strong&gt; — пет-проект, B2C-трекер привычек и целей. Backend на Spring Boot 3.5 + Java 21, Hexagonal Architecture на 6 Maven-модулях, jOOQ вместо JPA, Apache Kafka с шестью независимыми консьюмерами и идемпотентностью через таблицу &lt;code&gt;processed_events&lt;/code&gt;, JWT RS256 с ротацией refresh-токенов, OpenAPI 3.1 как источник правды для API. Frontend на React 19 + TypeScript 5.9, Vite 8, Tanstack React Query, Zustand, shadcn/ui + Tailwind CSS, тёмная тема, двуязычный интерфейс через react-i18next, мобильная адаптация до 375px.&lt;/p&gt;&lt;p&gt;В цифрах:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;17 спринтов SDD&lt;/strong&gt; — 7 на backend, 10 на frontend.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Два репозитория&lt;/strong&gt; — &lt;code&gt;lifesync-backend&lt;/code&gt; (v1.0.2) и &lt;code&gt;lifesync-frontend&lt;/code&gt; (v1.3.0). Деплой: backend на Railway, frontend на Vercel.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;149 000 слов спецификаций.&lt;/strong&gt; На backend — около 61 000 слов на 6 фич (где третий спринт прошёл без SDD-артефактов), на frontend — около 89 000 слов на 10 фич. Это не считая &lt;code&gt;constitution.md&lt;/code&gt; обоих репозиториев и кода как такового.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;251 тест на backend&lt;/strong&gt; (170 unit + 81 integration на Testcontainers с PostgreSQL и Kafka) и &lt;strong&gt;77 на frontend&lt;/strong&gt; (55 unit на Vitest + 22 E2E на Playwright).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;19 Liquibase-миграций&lt;/strong&gt;, JaCoCo подключён.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Релиз в продакшен&lt;/strong&gt; — backend на Railway (без Kafka в demo-окружении ради бесплатного тира), frontend на Vercel: &lt;a href="http://lifesync-frontend-ten.vercel.app" rel="noopener noreferrer nofollow"&gt;lifesync-frontend-ten.vercel.app&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Дальше — про то, что не дало этому всему разъехаться.&lt;/p&gt;&lt;h3&gt;Опора 1. Три чата вместо двух&lt;/h3&gt;&lt;p&gt;В первой статье я зафиксировал схему &lt;strong&gt;«два чата»&lt;/strong&gt;: отдельный чат с Claude — для думания (обсуждение архитектурных решений, подготовка промтов), Claude Code в терминале IDE — для исполнения (генерация спек, написание кода, запуск команд).&lt;/p&gt;&lt;p&gt;На FullStack-проекте эта схема не выдержала. Backend и frontend живут в разных репозиториях, у них разные стеки, разные конституции, разные циклы спринтов. Нельзя было держать оба контекста в одной голове и в одном думающем чате — рано или поздно я начал бы переносить решения с одного стека на другой по инерции.&lt;/p&gt;&lt;p&gt;В итоге сложилась схема &lt;strong&gt;«три чата»&lt;/strong&gt;, не считая Claude Code в терминале IDE — он по-прежнему остаётся исполнителем в каждом репозитории:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Думающий чат для backend.&lt;/strong&gt; Здесь живёт всё, что касается серверной части: Hexagonal-структура, Kafka-консьюмеры, JWT, jOOQ-запросы. Контекст этого чата — &lt;code&gt;lifesync-backend/constitution.md&lt;/code&gt; + OpenAPI-спецификация + актуальная фича в работе.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Думающий чат для frontend.&lt;/strong&gt; Сюда уходят React, TypeScript, React Query, shadcn/ui, темизация, i18n. Контекст — &lt;code&gt;lifesync-frontend/constitution.md&lt;/code&gt; + ссылки на backend-репо и Swagger UI, чтобы я мог обращаться к актуальному API-контракту.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Третий, координационный чат.&lt;/strong&gt; Для тем, которые касаются обоих репозиториев одновременно: деплой (Railway + Vercel + переменные окружения), кросс-репо фичи (например, сложная серверная валидация пароля с зеркальной клиентской), ретроспективы. В этом чате я готовил промты для двух других чатов, чтобы решения по общим фичам были согласованы.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;То есть концепция «два чата на проект» из первой статьи сохраняется — думающий чат плюс Claude Code в терминале. Просто к ним добавился координационный, общий для двух репозиториев. Итого пять рабочих контекстов: думающий + Claude Code на backend, то же на frontend, и координационный на двоих.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Когда я открывал второй чат для frontend&lt;/strong&gt;, я переносил в него контекст backend через явные промты: ссылку на репозиторий, ссылку на Swagger UI, файл OpenAPI-спецификации, краткое описание архитектуры. Это работает как &lt;strong&gt;информационный мостик&lt;/strong&gt; — frontend-чат получает не «общую идею», а конкретные точки опоры.&lt;/p&gt;&lt;p&gt;Что важно про этот мостик: он &lt;strong&gt;однонаправленный&lt;/strong&gt;. Backend-чат не получает обратной связи от frontend-чата автоматически — если в процессе разработки UI я обнаруживал несоответствие в API, я возвращался в координационный чат, прорабатывал там, и оттуда уже шёл новый промт в backend-чат. Это медленнее, чем «один чат на всё», но это и есть та структура, которая не даёт двум стекам перепутаться.&lt;/p&gt;&lt;p&gt;Третий чат — это не просто удобство. Это &lt;strong&gt;разделение зон ответственности на уровне ИИ-собеседника&lt;/strong&gt;. Когда вопрос конкретно про backend — backend-чат уже в контексте, не надо его поднимать с нуля. Когда вопрос про деплой — координационный чат знает обе стороны.&lt;/p&gt;&lt;h3&gt;Опора 2. Две конституции, живущие по-разному&lt;/h3&gt;&lt;p&gt;Конституция в SDD — это &lt;code&gt;.specify/memory/constitution.md&lt;/code&gt;, документ-договор между мной и проектом: какие архитектурные принципы соблюдаются, какие технологии в стеке, какие правила разработки. Spec Kit генерирует на её основе подсказки для всех остальных команд: &lt;code&gt;speckit.specify&lt;/code&gt;, &lt;code&gt;speckit.plan&lt;/code&gt;, &lt;code&gt;speckit.tasks&lt;/code&gt; опираются на конституцию, чтобы не предлагать решения, противоречащие зафиксированным правилам.&lt;/p&gt;&lt;p&gt;Для двух репозиториев нужны &lt;strong&gt;две разные конституции&lt;/strong&gt;. Для меня это было очевидно с самого начала — соблазна смешать всё в один документ не возникало. Два проекта — две конституции, каждая со своим набором принципов, своими стандартами кода, своей историей правок.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Backend constitution.&lt;/strong&gt; Стартовая ратификация — пустой шаблон в первый день проекта, затем версия 1.1.0 с полным набором принципов. Дальше — 12 правок, документ вырос от 50 строк до 437. Что добавлялось:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;v1.2.0 — стандарт работы с Liquibase (формат миграций, нумерация, структура changelog).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;v1.2.1 — правила атомарных коммитов.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;v1.2.2 — порядок code style правил.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;v1.2.4 — naming convention для веток.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;v1.3.0 — стандарт документирования OpenAPI как двенадцатый принцип.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;v1.3.1, v1.3.2 — execution rules: как именно интерпретировать &lt;code&gt;tasks.md&lt;/code&gt; при имплементации.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;После Sprint 5 (Kafka) &lt;code&gt;constitution.md&lt;/code&gt; backend больше не правился. Это &lt;strong&gt;не значит, что он умер&lt;/strong&gt; — это значит, что &lt;strong&gt;в раннюю фазу я зафиксировал все правила, которые регулируют дальнейшую разработку&lt;/strong&gt;. Когда правила работают, документ перестаёт расти. Это и есть здоровое состояние.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Frontend constitution.&lt;/strong&gt; Стартовая версия — 50 строк, затем ратификация 1.0.0 со всеми пятью принципами (API-Layer Isolation, Server State via React Query, Component-Logic Separation, Type Safety NON-NEGOTIABLE, Design System Fidelity). Дальше — всего 4 коммита. Финальный размер — 137 строк.&lt;/p&gt;&lt;p&gt;Эволюция точечная: смена React Router v6 → v7 в Technology Constraints, позже — расширение под i18n с добавлением &lt;code&gt;src/locales/&lt;/code&gt; в обязательную структуру (Sprint 9). Никаких больших переписываний.&lt;/p&gt;&lt;p&gt;Почему две конституции эволюционируют так по-разному? Потому что &lt;strong&gt;бэкенд решает больше неочевидных вопросов на старте&lt;/strong&gt;. Какой формат миграций? Как нумеровать коммиты? Какая структура OpenAPI? Каждый раз, когда в спринте всплывал новый вопрос, к которому конституция не давала ответа — я добавлял туда правило. На фронте таких вопросов было меньше: shadcn/ui задаёт паттерны, React Query — серверное состояние, TypeScript строгий — а остальное оказалось в существующих принципах.&lt;/p&gt;&lt;p&gt;Здесь важен один тезис: &lt;strong&gt;конституция — это живой документ&lt;/strong&gt;. Когда в проекте появляется новая ситуация, не покрытая правилами — документ адаптируется. Это не «нарушение» — это часть методологии. Spec Kit прямо об этом пишет: конституция должна быть живой, иначе она становится мёртвой буквой, которую все игнорируют.&lt;/p&gt;&lt;p&gt;Adapting the rule, not breaking it. Разница принципиальная.&lt;/p&gt;&lt;h3&gt;Опора 3. speckit.analyze как контур обратной связи&lt;/h3&gt;&lt;p&gt;&lt;code&gt;speckit.analyze&lt;/code&gt; — одна из шести команд Spec Kit, и формально она называется «cross-artifact consistency analysis report». На практике это команда, которая проверяет согласованность между четырьмя артефактами фичи: &lt;code&gt;spec.md&lt;/code&gt;, &lt;code&gt;plan.md&lt;/code&gt;, &lt;code&gt;tasks.md&lt;/code&gt;, и фактическим кодом после имплементации. Если в спецификации описано требование, которое не отражено в плане, или в плане есть фаза, не разложенная на задачи, или код реализован в обход того, что было спланировано — &lt;code&gt;analyze&lt;/code&gt; это находит.&lt;/p&gt;&lt;p&gt;Я запускаю &lt;code&gt;speckit.analyze&lt;/code&gt; &lt;strong&gt;после реализации задач спринта&lt;/strong&gt; — это обязательная часть моего цикла. Иногда — ещё и &lt;strong&gt;перед реализацией&lt;/strong&gt;, после того как появился &lt;code&gt;tasks.md&lt;/code&gt;, когда хочется убедиться, что задачи действительно закрывают спецификацию. За 17 спринтов &lt;code&gt;analyze&lt;/code&gt; ловил несколько критичных вещей и регулярные мелкие. Самый яркий пример — Sprint 5 backend, события через Kafka.&lt;/p&gt;&lt;p&gt;Я закончил имплементацию шести консьюмеров со встроенной идемпотентностью, прогнал тесты, всё зелёное. Запустил &lt;code&gt;speckit.analyze&lt;/code&gt; post-impl. Нашёлся неочевидный недочёт: в нескольких use case'ах публикация события в Kafka шла внутри транзакции, без try-catch вокруг &lt;code&gt;publishEvent&lt;/code&gt;. Это значило, что если Kafka в моменте недоступна — исключение поднимается наверх и &lt;strong&gt;откатывает успешно зафиксированную транзакцию БД&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;То есть данные сохранились, потом откатились, событие не отправилось — система в неконсистентном состоянии.&lt;/p&gt;&lt;p&gt;Я бы это в комит-ревью пропустил. Тесты были зелёные, потому что в Testcontainers Kafka всегда доступна. Сценарий «Kafka недоступна, БД доступна» в integration-тестах не покрывался. &lt;code&gt;speckit.analyze&lt;/code&gt; это поймал не потому, что прогнал какой-то умный тест — а потому, что прошёлся по спецификации, увидел требование «события публикуются после успешной транзакции», и сравнил его с фактом «публикация внутри транзакции». Расхождение.&lt;/p&gt;&lt;p&gt;Поправил try-catch, добавил тест на DLQ retry, и заодно — асинхронный IT-тест на streak-расчёт, который тоже всплыл в этом анализе.&lt;/p&gt;&lt;p&gt;Это &lt;strong&gt;не магия&lt;/strong&gt;. Это формализованный контур обратной связи: каждая фича заканчивается тем, что артефакты сравниваются между собой и с кодом. Расхождения сигнализируются. Я их разбираю и фиксирую. Это ловит вещи, которые иначе уехали бы в продакшен.&lt;/p&gt;&lt;p&gt;Ключевое наблюдение про &lt;code&gt;analyze&lt;/code&gt;: &lt;strong&gt;он не заменяет тесты или ревью. Он работает на другом уровне&lt;/strong&gt; — на уровне согласованности артефактов между собой. Тесты проверяют, что код делает что-то. &lt;code&gt;analyze&lt;/code&gt; проверяет, что код делает &lt;strong&gt;то, что было обещано в спецификации&lt;/strong&gt;. Это разные слои.&lt;/p&gt;&lt;h3&gt;Опора 4. API-first как договор между двумя репозиториями&lt;/h3&gt;&lt;p&gt;Когда два репозитория существуют отдельно, главная угроза согласованности — &lt;strong&gt;API-контракт&lt;/strong&gt;. Если backend выкатывает новый endpoint и забывает про frontend, или frontend ожидает поле, которое в API называется иначе — координация ломается. На обычной командной разработке это решается переписками, JIRA-тикетами, ревью. У меня была другая опция: API-first.&lt;/p&gt;&lt;p&gt;В backend я завёл отдельный Maven-модуль &lt;code&gt;lifesync-api-spec&lt;/code&gt;, в котором лежит единственный файл — &lt;code&gt;lifesync-api.yaml&lt;/code&gt;, OpenAPI 3.1 спецификация на все endpoint'ы. На сегодня это &lt;strong&gt;2669 строк YAML, 32 endpoint'а&lt;/strong&gt;. Backend генерирует Java-интерфейсы из этого YAML через &lt;code&gt;openapi-generator-maven-plugin&lt;/code&gt; (контроллеры реализуют сгенерированные интерфейсы — писать их вручную запрещено конституцией). Frontend читает тот же YAML как &lt;strong&gt;источник правды&lt;/strong&gt; и пишет TypeScript-типы в &lt;code&gt;src/types/&lt;/code&gt; руками: 5 файлов, около 230 строк типов.&lt;/p&gt;&lt;p&gt;Почему frontend пишет типы руками, а не генерирует? Это сознательное решение. Генератор TypeScript-типов из OpenAPI — рабочая опция (&lt;code&gt;openapi-typescript&lt;/code&gt;, &lt;code&gt;orval&lt;/code&gt;, &lt;code&gt;@hey-api&lt;/code&gt;), и я её рассматривал. Не пошёл по двум причинам:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Контроль над формой типов.&lt;/strong&gt; Сгенерированные типы часто буквально повторяют структуру API, со всей nullability и opt'ами. Для UI часто удобнее немного перекомпонованный тип, который ближе к тому, что отрисовывается на экране. Ручное написание оставляет это под контроль.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Типов мало, изменений ещё меньше.&lt;/strong&gt; 230 строк типов на 32 endpoint'а — это контролируемый объём. Каждое изменение API сопровождалось ручной правкой типов на фронте, и это не превращалось в трудоёмкую задачу.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Внутри &lt;code&gt;specs/&lt;/code&gt; каждой фичи фронта лежит &lt;strong&gt;локальный contracts/-каталог&lt;/strong&gt; с markdown-описанием endpoint'ов, которые эта фича использует — &lt;code&gt;auth-api.md&lt;/code&gt;, &lt;code&gt;goals-api.md&lt;/code&gt;, &lt;code&gt;habits-api.md&lt;/code&gt;. Это не дублирование YAML и не альтернативный контракт, а &lt;strong&gt;навигационный срез под конкретную фичу&lt;/strong&gt; для думающего чата: какие endpoint'ы я зову, какие поля жду, какие ошибки могут прилететь. Источник правды — по-прежнему YAML на 2669 строк, но его не нужно держать перед глазами целиком, когда работаешь над одной фичей.&lt;/p&gt;&lt;p&gt;Эта схема работает как простой договор: &lt;strong&gt;один YAML — источник правды, два репозитория его читают, координация идёт через документ, а не через переписку&lt;/strong&gt;. Когда мне нужно поменять API — я меняю YAML, перегенерирую интерфейсы на бэке, реализую методы, потом правлю типы на фронте. Если что-то рассинхронилось — это видно сразу, потому что TypeScript падает на компиляции.&lt;/p&gt;&lt;p&gt;Что важно понимать про эту схему: &lt;code&gt;&lt;strong&gt;speckit.analyze&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; не проверяет согласованность между репозиториями автоматически&lt;/strong&gt;. Каждая команда Spec Kit работает в пределах одного репозитория. Я делал кросс-репо проверки руками: после изменений API в backend — в координационном чате готовил промт «вот изменения в API, проверь типы и компоненты frontend на необходимость правок», и проходил по этому промту в frontend-чате.&lt;/p&gt;&lt;h3&gt;Где было трудно&lt;/h3&gt;&lt;p&gt;Несколько мест за 17 спринтов, где SDD пробуксовывал — и где я остановился, переразобрался, продолжил.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Sprint 8 backend, рассинхрон plan.md vs spec.md.&lt;/strong&gt; &lt;code&gt;speckit.analyze&lt;/code&gt; показал расхождение: &lt;code&gt;plan.md&lt;/code&gt; описывал три фазы имплементации, в &lt;code&gt;spec.md&lt;/code&gt; была упомянута четвёртая. По коду я уже сделал все четыре, но в плане — синхронизации не было. Случай несложный (одна правка в &lt;code&gt;plan.md&lt;/code&gt;), но показательный: &lt;strong&gt;методология не позволила мне забыть про эту синхронизацию&lt;/strong&gt;. Без &lt;code&gt;analyze&lt;/code&gt; я бы заметил это только в ретроспективе спустя три спринта.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Sprint 10 frontend, релизный с накопленными доработками.&lt;/strong&gt; Это был последний спринт фронта, и он отличался от предыдущих: не одна большая фича, а набор мелких UX-улучшений и багфиксов, которые накопились перед релизом. SDD-цикл здесь работал иначе — короткий &lt;code&gt;spec.md&lt;/code&gt;, плотный &lt;code&gt;tasks.md&lt;/code&gt; на список доработок, обычный &lt;code&gt;analyze&lt;/code&gt;в конце. Главный урок: &lt;strong&gt;не каждый спринт идёт по идеальному циклу&lt;/strong&gt;. Иногда фича — это «накопленные мелочи перед релизом», и она требует своих tasks.md и analyze, но без больших спецификаций.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Backend Sprint 3, который прошёл мимо SDD.&lt;/strong&gt; В backend между Sprint 2 и Sprint 4 был короткий спринт настройки локального окружения для разработки — один fix-коммит, без &lt;code&gt;specs/003-*/&lt;/code&gt;. Тогда я не делал спецификацию: задача казалась слишком технической для SDD-цикла. Сегодня бы я сделал спеку даже на это — &lt;code&gt;specs/003-local-dev-setup/spec.md&lt;/code&gt; со списком требований к dev-окружению занял бы 50 строк, но дал бы фиксацию: какие порты, какие переменные окружения, какой docker-compose. Урок на будущее: SDD одинаково полезен на больших фичах и на маленьких настроечных задачах.&lt;/p&gt;&lt;h3&gt;Что я забрал из этого опыта&lt;/h3&gt;&lt;p&gt;Чувство контроля на 17 спринтах сохранилось до последней фичи. Это не потому, что я какой-то особенный — это потому, что методология даёт &lt;strong&gt;систему обратной связи&lt;/strong&gt; на нескольких уровнях одновременно.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Спецификации&lt;/strong&gt; фиксируют намерение. Что я хотел построить.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Конституция&lt;/strong&gt; фиксирует правила. Как я договорился это строить.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;&lt;strong&gt;speckit.analyze&lt;/strong&gt;&lt;/code&gt; ловит расхождения. Что разъехалось между намерением, правилами и кодом.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Три чата&lt;/strong&gt; разделяют контексты. Backend, frontend, общая координация — каждый со своим документальным фундаментом.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;API-first&lt;/strong&gt; — договор между двумя репозиториями. Один YAML, два потребителя.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Когда эти пять компонентов работают вместе, масштаб проекта &lt;strong&gt;перестаёт быть угрозой управляемости&lt;/strong&gt;. Спринтов может быть 17, может быть 50 — структура контроля не меняется. Меняется только содержание.&lt;/p&gt;&lt;p&gt;Главный сдвиг от первой статьи: &lt;strong&gt;методология масштабируется без потери качества.&lt;/strong&gt; Один вечер с Telegram-ботом и 17 спринтов с FullStack-приложением — это одна и та же методология, отличающаяся в деталях (третий чат, две конституции вместо одной), но не в сути. Это и есть тот ответ на вопрос «работает ли SDD за пределами игрушек», который у меня после первой статьи был ещё открытым.&lt;/p&gt;&lt;p&gt;Сейчас он закрыт. Работает. Я готов делать следующий проект и расширять существующие через ту же методологию — и я знаю, на каких местах нужно держать голову острее, чтобы масштаб не растащил процесс.&lt;/p&gt;&lt;h3&gt;Что дальше&lt;/h3&gt;&lt;p&gt;Это третья статья в серии материалов из моих проектов:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://habr.com/ru/articles/1027250/" rel="noopener noreferrer nofollow"&gt;Первая&lt;/a&gt; — про SDD на одном вечере на примере Telegram-бота.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a href="https://habr.com/ru/articles/1027742/" rel="noopener noreferrer nofollow"&gt;Вторая&lt;/a&gt; — технический разбор архитектурных решений на парсере бизнес-формул через ANTLR4.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;В следующих статьях планирую разбирать конкретные технические темы из LifeSync:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hexagonal Architecture на Maven Multi-Module&lt;/strong&gt; — как разложить backend на шесть модулей с чистым &lt;code&gt;domain&lt;/code&gt; без Spring и use case'ами через &lt;code&gt;@Bean&lt;/code&gt; в конфигурации.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;jOOQ вместо Hibernate&lt;/strong&gt; — почему я отказался от ORM и где это окупается, а где нет.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Репозитории проекта:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Backend — &lt;a href="https://github.com/zahaand/lifesync-backend" rel="noopener noreferrer nofollow"&gt;github.com/zahaand/lifesync-backend&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Frontend — &lt;a href="https://github.com/zahaand/lifesync-frontend" rel="noopener noreferrer nofollow"&gt;github.com/zahaand/lifesync-frontend&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Демо — &lt;a href="http://lifesync-frontend-ten.vercel.app" rel="noopener noreferrer nofollow"&gt;lifesync-frontend-ten.vercel.app&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Только зарегистрированные пользователи могут участвовать в опросе. &lt;a rel="nofollow" href="https://habr.com/kek/v1/auth/habrahabr/?back=&amp;amp;hl=ru"&gt;Войдите&lt;/a&gt;, пожалуйста.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;На каком масштабе вы пробовали SDD?&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;Не пробовал — пока изучаю&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;Маленький проект (один вечер / один спринт)&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;Несколько спринтов&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;Длительная разработка (10+ спринтов)&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;0%&lt;/span&gt;&lt;span&gt;Использую SDD регулярно как основной процесс&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt; Никто еще не голосовал.   Воздержавшихся нет. &lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">zahaand</dc:creator><pubDate>Mon, 27 Apr 2026 05:00:14 +0000</pubDate><guid>https://habr.com/ru/articles/1027886/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=1027886</guid><category>spec-driven development</category><category>spec kit</category><category>claude code</category><category>ai-assisted development</category><category>fullstack</category><category>java</category><category>spring boot</category><category>react</category><category>методология</category><category>architecture</category></item></channel></rss>