SubscribersHow many subscribers the channel has now. The wave shows subscribers at each point of the period. The change is against the start of the period.2 180▲ 0.5 %+11 for the period
ViewsHow many views the posts published in the period have now. These are not views per day: MAX does not report how many views there were yesterday. The percentage is the change in the daily average: the current period is still running (today is not over yet), so it is compared with the previous one by pace rather than by total. “Was” is the whole previous period of the same length.283▼ 35.1 %was 441
ReactionsHow many reactions the posts published in the period have. The percentage is the change in the daily average: the current period is still running (today is not over yet), so it is compared with the previous one by pace rather than by total. “Was” is the whole previous period of the same length.1▼ 83.1 %was 6
MentionsHow many times other channels mentioned the channel: links and reposts, excluding advertising posts. The bigger the circle, the more mentions at that point of the period. The percentage is the change in the daily average: the current period is still running (today is not over yet), so it is compared with the previous one by pace rather than by total. “Was” is the whole previous period of the same length.61▼ 19.5 %was 76with a subscription
RepostsHow many times other channels reposted this channel's posts (excluding advertising posts). Reposts are also counted as mentions. The percentage is the change in the daily average: the current period is still running (today is not over yet), so it is compared with the previous one by pace rather than by total. “Was” is the whole previous period of the same length.28▲ 12.3 %was 25after signing in
Average post reachHow many views one post gets on average. Only posts older than two days are counted: views of fresh posts are still growing. Points with fresh posts are dashed with hollow dots: the reach of all posts right now, it will still grow.5 535▲ 2.9 %was 5 381after signing in
ERRWhat share of subscribers sees an average post: average reach ÷ subscribers × 100 %. The line shows ERR at each point of the period, the dashed line — ERR of the previous period of the same length. Points with posts younger than two days are hollow dots on the dashed end of the line.29.0 %▲ 0.1 ppdashed line — was 28.9 %after signing in
Engagement ERHow often readers react to posts: reactions ÷ views × 100 %. The tick on the bar is the previous period.2.80 %▼ 0.2 pp
Posts per dayHow many posts are published per day on average. Each cell is a point of the period: the darker, the more posts per day. The percentage is the change in the daily average: the current period is still running (today is not over yet), so it is compared with the previous one by pace rather than by total. “Was” is the whole previous period of the same length.4.3▼ 8.9 %was 4.7after signing in
Ad shareWhat share of the period's posts looks like advertising: erid labeling, ad links and other signs. The scale shows the ad load by the quality index thresholds: up to 10 % — low, up to 25 % — moderate, up to 50 % — high, above — very high. A growing share is marked red.3.9 %▲ 1.8 pp
TrendTop — subscribers at the end of each point of the period; bottom — growth per point: green means more subscribers, red means fewer. Above the growth chart — the biggest gain and the biggest loss. MAX does not report how many people subscribed and unsubscribed separately.
Audience growth
+11subscribers in the period
+3 biggest gain · 26 Oct 2026
Channel scoreA channel score from 0 to 100 made of five parts: audience, growth integrity, ad load, trust and calculation reliability. Each bar shows how many points the part earned out of its maximum. The index is recalculated every night; channels are compared with similar ones of the same category and size.
ReachHow many views the posts published at each point of the period have now (not views per day). The record is labeled; the point with today is paler — the day is not over yet. Average post reach — posts older than two days.
Views and reach
709 891views in the period▼ 6.8 %
Average post reach — 5 535 views (posts older than two days).
EngagementER — reactions divided by views, in percent — for the posts of each point of the period. The dashed line is ER for the whole period. The change is in percentage points against the previous period of the same length.
CitationsThe citation index (CI) shows how often and by which channels this channel is mentioned over the last 30 days, excluding advertising posts. The level is its place among similar channels. The line shows CI at each point of the period (for a week or month — the average over its days).
RhythmHow many posts are published on average in each hour of each weekday, Moscow time: the darker the cell, the more posts. “Best time” — the hours when posts older than two days have the highest average reach.
When and how much it posts
MonTueWedThuFriSatSun
036912151821
Best time:09:0018:00hours when posts get the highest average reach
FormatsWhat the feed is made of. Each post has one format, by priority: repost, video, photo, link, text. The bar and percentages show the share of posts of each format; on the right — average reach of one post of the format (posts older than two days), the small bar compares it with the most viewed format.
Post formats
100 posts
Reposts99 %6 168
Video2121 %7 513
Photos3838 %5 699
Links1212 %5 239
Text2020 %7 626
On the right — average reach of one post of the format.
PostsEach dot is a post in the period: horizontally — publication date, vertically — views, dot size — reactions, color — format. This shows hits, flops and how reach changes. For large channels, the 100 most viewed posts and an even sample of the rest are shown — no more than 1,500 dots.
Every post is a dot
Higher means more views, bigger means more reactions. Click a dot to open the post.
VideoPhotosOthersize — reactionsZoom with the + and − buttons, a pinch or Ctrl + mouse wheel; drag a zoomed chart to move it
SummaryTotals for the selected period: how many posts were published, how many views and reactions they have now and the average post reach. Views are the current views of posts published on those days, not views per day. The change is against the same span before the period, if statistics cover it.
Period totals
Click a metric to see it across the period below
Viewstoday — the day is not over yetPoints with posts younger than two days are pale and dashed: their views are still growing.
AdsWhat share of the period's posts looks like advertising: erid labeling, ad links and other signs. The detector is cautious, so the real share is usually higher. The scale shows the ad load by the quality index thresholds. Below — the average reach of an ad post and a regular post, and who the channel advertises (sites that ad links lead to).
What worksPosts of the period with the most views. The bar shows views relative to first place. The preview is the post's image; for a post without an image — the channel avatar. Views are the post's current count.
RhythmHow many posts were published each day of the period (pale bars) and the 7-day average (line): you can see whether the channel is speeding up or slowing down. Today is not over yet, so its average is calculated up to yesterday.
Posts by day
Pale bars are days, the line is the 7-day average: you can see whether the channel is speeding up or slowing down
Posts per day7-day average
Data updated at 22:02 Moscow time. Views are the current counts of posts published on those days; average reach counts posts older than two days.
How the quality index is calculated
📝 log/slog: Хватит парсить логи глазами (и тащить Logrus в проект)
Два часа ночи. Прод лежит. Вы сидите в терминале и пытаетесь выцепить причину через grep "error" | grep "user_1234". Знакомая жиза?
Долгие годы стандартный пакет log в Go был... ну, скажем честно, спартанским. Он писал простой текст. А современные системы мониторинга (ELK, Datadog, Grafana Loki) любят структурированные данные (JSON или Key-Value), чтобы по ним можно было строить графики и алерты.
Поэтому каждый Go-разработчик тащил в проект сторонние комбайны: logrus (который официально в режиме maintenance с 2020 года), zerolog или uber-go/zap.
С выходом Go 1.21 праздник наступил и на нашей улице - в стандартную библиотеку завезли log/slog.
Почему slog - это круто? Он быстрый, расширяемый и, главное, избавляет от сторонних зависимостей.
Самая частая проблема логирования, случайно слить в логи пароль, токен или кредитную карту (PII данные), передав структуру целиком. В slog есть гениальная вещь - интерфейс LogValuer. Если ваша структура его реализует, логгер вызовет его до сериализации. type User struct { ID int Name string Password string // СЕКРЕТ! }
// Учим структуру безопасно логировать саму себя func (u User) LogValue() slog.Value { return slog.GroupValue( slog.Int("id", u.ID), slog.String("name", u.Name), slog.String("password", "***REDACTED***"), // Скрыли! ) }
// Даже если джун случайно залогирует юзера целиком: slog.Info("created user", slog.Any("user", user)) // В логах будет: "password": "***REDACTED***". Прод спасен. }
Конечно, zap всё ещё чуть быстрее на экстремальных нагрузках благодаря жесткой кодогенерации, но для 95% микросервисов slog - это новый абсолютный стандарт.
👾 ЛЦТ.Фест 2026 – знакомьтесь с технологиями, которые меняют город и бизнес
30 октября в кластере «Ломоносов» в Москве встретятся студенты ИТ- и инженерных направлений, специалисты технологической отрасли и все, кому интересны современные цифровые разработки России.
В программе: выступления экспертов, интерактивные площадки и знакомство с решениями для города и бизнеса. Приходите узнать больше о технологиях, пообщаться с представителями индустрии, узнать о стажировках и вакансиях в крупных компаниях, найти единомышленников и узнать, кто разделит призовой фонд хакатона «Лидеры цифровой трансформации» в 40 млн рублей.
🧠 Go Memory Model: Почему ваша горутина слепая (и что такое Happens-Before)
Напишем простейший код. Одна горутина меняет флаг, другая ждет этого изменения:
var done bool var msg string
func setup() { msg = "hello, world" done = true }
func main() { go setup()
// Ждем, пока setup() закончит работу for !done { }
fmt.Println(msg) }
Джун посмотрит на этот код и скажет: "Ну, он выведет hello, world". Сеньор посмотрит на этот код и скажет: "Этот код может уйти в бесконечный цикл, может вывести пустую строку, а может работать нормально. Это лотерея".
Добро пожаловать в жестокий мир Go Memory Model.
Иллюзия мгновенной памяти
Мы привыкли думать, что память - это единый монолит. Записал переменную - она тут же стала доступна всем. Но в реальности код выполняется на современных многоядерных процессорах. У каждого ядра есть свой сверхбыстрый локальный кэш (L1/L2).
Когда горутина setup пишет done = true, процессор может положить это значение в свой локальный кэш и не сбрасывать его в общую оперативную память (RAM) еще миллисекунды. А горутина main, работающая на другом ядре, будет бесконечно читать старое значение done == false из своего собственного кэша.
Но и это не всё! Компилятор Go невероятно умен. Он смотрит на цикл for !done {} и думает: "Внутри этого цикла переменная done не меняется. Зачем мне читать её из памяти на каждой итерации? Прочитаю один раз, сохраню в регистр процессора и буду проверять его". Итог - вечный цикл (Infinite Loop).
Святой Грааль: Happens-Before (Происходит-До)
Чтобы навести порядок в этом хаосе, создатели Go написали формальный документ - Модель Памяти (Memory Model). В её основе лежит концепция Happens-Before.
Если событие A происходит до события B, то Модель Памяти гарантирует, что горутина, выполняющая B, абсолютно точно увидит изменения, сделанные в A.
Если между действиями нет связи Happens-Before, компилятор и процессор имеют право переупорядочивать инструкции как им вздумается, а кэши могут не синхронизироваться.
Как создать связь Happens-Before?
Вам нужны примитивы синхронизации. Они работают как жесткие барьеры памяти (Memory Barriers), заставляя процессоры сбросить кэши, а компилятор - прекратить умничать с перестановкой строк кода.
✅ 1. Каналы (Channels) Отправка данных в канал происходит до завершения чтения из этого канала в другой горутине.
c := make(chan struct{}) go func() { msg = "hello" c <- struct{}{} // Happens-Before }() <-c // Ждем fmt.Println(msg) // 100% увидит "hello"
✅ 2. Мьютексы (Mutexes) Снятие блокировки mu.Unlock() происходит до успешного захвата mu.Lock() другой горутиной.
✅ 3. Атомики (sync/atomic) Начиная с Go 1.19, в Модель Памяти официально добавили пакет atomic. Атомарные операции теперь гарантированно создают связь Happens-Before.
✅ 4. Жизненный цикл горутины Создание горутины (go func()) происходит до начала выполнения её кода. А вот завершение горутины не имеет гарантий! (Именно поэтому нам нужен sync.WaitGroup, чтобы дождаться её финала).
🔥 Senior Tip: Не пытайтесь обмануть систему
Существует соблазн написать код, который работает "без блокировок", опираясь на задержки: time.Sleep(time.Millisecond). Это называется data race (состояние гонки). Даже если на вашей машине с процессором Intel код работает идеально, завтра его скомпилируют под ARM-архитектуру (Apple Silicon или AWS Graviton), у которой гораздо более слабая модель памяти процессора, и ваш код рассыплется в пыль.
Всегда используйте флаг -race во время локальной разработки и CI: go test -race ./.... Он ловит отсутствие связей Happens-Before с хирургической точностью.
🛑 Graceful Shutdown: Хватит обрывать запросы при деплое
Знакомая ситуация: вы катите новую версию микросервиса в Kubernetes, и в этот момент графики в Grafana на пару секунд краснеют от 502/503 ошибок. А потом всё снова хорошо.
Что произошло? Оркестратор убил старый под, а ваш Go-сервис моментально схлопнулся, оборвав на полуслове десятки текущих HTTP-запросов, сломав пару транзакций к базе и бросив недокачанные файлы.
Как это работает в здоровом проде? Когда Kubernetes хочет обновить под, он не стреляет ему в голову сразу. Он посылает вежливый сигнал SIGTERM и дает время (обычно 30 секунд), чтобы собрать вещи. Мы должны перехватить этот сигнал и сказать нашему HTTP-серверу: "Новые запросы не принимай, а те, что уже в работе - спокойно доделывай".
Как это написать: Забудьте про ручное создание make(chan os.Signal). Используйте signal.NotifyContext.
func main() { // 1. Создаем контекст, который отменится при SIGINT (Ctrl+C) или SIGTERM (Docker/K8s) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop()
srv := &http.Server{Addr: ":8080", Handler: mux}
// 2. Запускаем сервер в отдельной горутине! go func() { // http.ErrServerClosed возвращается при успешном Shutdown, это не ошибка if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("Критическая ошибка сервера: %v", err) } }()
// 3. Блокируемся и ждем сигнала от ОС <-ctx.Done() log.Println("Получен сигнал завершения. Тушим свет...")
// 4. Создаем контекст с таймаутом для Shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
// 5. Плавно останавливаем сервер (ждет завершения текущих хендлеров) if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("Сервер не смог остановиться плавно: %v", err) }
// 6. Здесь закрываем коннекты к БД, RabbitMQ, сбрасываем кэши в Redis // db.Close()
log.Println("Сервер успешно завершил работу.") }
🔥 Нюансы для Senior-ов: • Обязательно ставьте таймаут на Shutdown(). Если клиент с медленным интернетом качает от вас гигабайтный файл, srv.Shutdown будет ждать его вечно. В итоге K8s устанет ждать свои 30 секунд и жестко убьет под через SIGKILL (kill -9). Контролируйте таймаут сами (Шаг 4).
• Порядок важен. Сначала останавливаем HTTP-сервер (чтобы перестали прилетать новые задачи), и только потом закрываем пул соединений с БД. Если сделать наоборот, текущие HTTP-запросы упадут с ошибкой sql: database is closed.
🐳 Идеальный Dockerfile для Go. Худеем с 800 МБ до 10 МБ
Знакомая картина: джун приносит микросервис, который делает пару запросов к БД, а Docker-образ весит под гигабайт. Почему? Потому что в FROM гордо красуется golang:latest.
Вместе с вашим кодом в продакшен уезжает весь тулчейн компилятора, операционная система, куча утилит (шелл, curl) и, как бонус, пачка потенциальных уязвимостей.
Прелесть Go в том, что он компилирует всё в один независимый бинарный файл. Нам не нужен Go в рантайме! Наш путь - Multi-stage builds (Многоэтапная сборка).
Вот "золотой стандарт" Dockerfile, который работает в 99% случаев:
# === Этап 1: Сборка (Builder) === FROM golang:1.22-alpine AS builder
WORKDIR /app
# Шаг 1: Кэшируем зависимости # Этот слой пересоберется ТОЛЬКО если изменились go.mod или go.sum COPY go.mod go.sum ./ RUN go mod download
# Шаг 2: Копируем код COPY . .
# Шаг 3: Собираем бинарник # CGO_ENABLED=0 - критически важно для статической линковки # -ldflags="-w -s" - вырезаем отладочную информацию RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/bin/service ./cmd/server/main.go
# === Этап 2: Финальный образ (Runner) === # Используем distroless вместо alpine или scratch FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
# Забираем только готовый бинарник из первого этапа COPY --from=builder /app/bin/service .
# Контейнер будет работать от непривилегированного пользователя USER nonroot:nonroot
EXPOSE 8080 ENTRYPOINT ["/app/service"]
Разбираем нюансы под капотом (Senior Edition):
• Зачем нужен CGO_ENABLED=0? По умолчанию Go может динамически линковаться с системными C-библиотеками (например, libc). Если вы соберете такой бинарник и положите в пустой образ, он упадет при старте. Отключая CGO, мы принудительно делаем статическую линковку - всё необходимое запекается прямо внутрь файла.
• Флаги -ldflags="-w -s": Они вырезают DWARF-таблицы и символы отладки. Это бесплатно срезает ~20-30% веса бинарника. Не переживайте, трейсы паник (stack traces) всё ещё будут показывать номера строк, так что для прода это безопасно.
• Почему distroless/static, а не scratch или alpine? * В scratch (абсолютно пустом образе) нет корневых сертификатов (ca-certificates). Как только ваш сервис попытается сделать внешний HTTPS-запрос, он упадет. Там также нет таймзон (tzdata). - В alpine есть sh (шелл) и пакетный менеджер apk - это векторы атаки для хакеров, если они найдут RCE в вашем коде. - Google Distroless содержит только сертификаты, таймзоны и юзера nonroot. Идеальный баланс микроскопического размера (~2 МБ) и безопасности.
У кого образы до сих пор весят больше 50 МБ - срочно переписывать!
🧠 Go Memory Model: Почему ваша горутина слепая (и что такое Happens-Before)
Напишем простейший код. Одна горутина меняет флаг, другая ждет этого изменения: var done bool var msg string
func setup() { msg = "hello, world" done = true }
func main() { go setup()
// Ждем, пока setup() закончит работу for !done { }
fmt.Println(msg) }
Джун посмотрит на этот код и скажет: "Ну, он выведет `hello, world`". Сеньор посмотрит на этот код и скажет: "Этот код может уйти в бесконечный цикл, может вывести пустую строку, а может работать нормально. Это лотерея".
Добро пожаловать в жестокий мир Go Memory Model.
Иллюзия мгновенной памяти
Мы привыкли думать, что память - это единый монолит. Записал переменную — она тут же стала доступна всем. Но в реальности код выполняется на современных многоядерных процессорах. У каждого ядра есть свой сверхбыстрый локальный кэш (L1/L2).
Когда горутина setup пишет done = true, процессор может положить это значение в свой локальный кэш и не сбрасывать его в общую оперативную память (RAM) еще миллисекунды. А горутина main, работающая на другом ядре, будет бесконечно читать старое значение done == false из своего собственного кэша.
Но и это не всё! Компилятор Go невероятно умен. Он смотрит на цикл for !done {} и думает: "Внутри этого цикла переменная done не меняется. Зачем мне читать её из памяти на каждой итерации? Прочитаю один раз, сохраню в регистр процессора и буду проверять его". Итог - вечный цикл (Infinite Loop).
Святой Грааль: Happens-Before (Происходит-До)
Чтобы навести порядок в этом хаосе, создатели Go написали формальный документ - Модель Памяти (Memory Model). В её основе лежит концепция Happens-Before.
> Если событие A происходит до события B, то Модель Памяти гарантирует, что горутина, выполняющая B, абсолютно точно увидит изменения, сделанные в A.
Если между действиями нет связи Happens-Before, компилятор и процессор имеют право переупорядочивать инструкции как им вздумается, а кэши могут не синхронизироваться.
Как создать связь Happens-Before?
Вам нужны примитивы синхронизации. Они работают как жесткие барьеры памяти (Memory Barriers), заставляя процессоры сбросить кэши, а компилятор - прекратить умничать с перестановкой строк кода.
✅ 1. Каналы (Channels) Отправка данных в канал происходит до завершения чтения из этого канала в другой горутине.
c := make(chan struct{}) go func() { msg = "hello" c <- struct{}{} // Happens-Before }() <-c // Ждем fmt.Println(msg) // 100% увидит "hello"
✅ 2. Мьютексы (Mutexes) Снятие блокировки mu.Unlock() происходит до успешного захвата mu.Lock() другой горутиной.
✅ 3. Атомики (sync/atomic) Начиная с Go 1.19, в Модель Памяти официально добавили пакет atomic. Атомарные операции теперь гарантированно создают связь Happens-Before.
✅ 4. Жизненный цикл горутины Создание горутины (go func()) происходит до начала выполнения её кода. А вот завершение горутины не имеет гарантий! (Именно поэтому нам нужен sync.WaitGroup, чтобы дождаться её финала).
🔥 Senior Tip: Не пытайтесь обмануть систему
Существует соблазн написать код, который работает "без блокировок", опираясь на задержки: time.Sleep(time.Millisecond). Это называется data race (состояние гонки). Даже если на вашей машине с процессором Intel код работает идеально, завтра его скомпилируют под ARM-архитектуру (Apple Silicon или AWS Graviton), у которой гораздо более слабая модель памяти процессора, и ваш код рассыплется в пыль.
Всегда используйте флаг -race во время локальной разработки и CI: go test -race ./.... Он ловит отсутствие связей Happens-Before с хирургической точностью.
📢 sync.Cond: Как разбудить 10 000 горутин одним вызовом (и не сломать планировщик)
Представьте задачу: у вас запущено 10 000 горутин-воркеров, которые должны начать обработку данных строго одновременно, но только после того, как в память загрузится тяжелый кэш или конфигурация.
Как передать им сигнал "на старт"?
Вариант 1: Каналы. Если мы отправим сигнал в канал ch <- struct{}{}, проснется только одна горутина. Чтобы разбудить 10 000, придется писать цикл. Долго и неэффективно. Можно сделать close(ch) - закрытие канала мгновенно разбудит все горутины, читающие из него. Но канал можно закрыть только один раз. Что если нам нужно подавать такой массовый сигнал каждые 5 минут при обновлении кэша?
Здесь на сцену выходит sync.Cond (Condition Variable) - один из самых недооцененных примитивов синхронизации.
Это рупор, через который одна горутина может крикнуть толпе спящих горутин: "Событие произошло, просыпайтесь!"
Как это работает
У sync.Cond есть три главных метода:
* Wait() - усыпить текущую горутину до получения сигнала. * Signal() - разбудить одну случайную спящую горутину. * Broadcast() - разбудить все спящие горутины разом.
Чтобы sync.Cond работал, ему обязательно нужен мьютекс (sync.Mutex или sync.RWMutex), который защищает условие.
✅ Как это выглядит в коде:
type AppState struct { mu sync.Mutex cond *sync.Cond configLoad bool }
func NewAppState() *AppState { a := &AppState{} // Cond всегда связывается с мьютексом a.cond = sync.NewCond(&a.mu) return a }
func (a *AppState) Worker(id int) { a.mu.Lock() // Обязательно лочим мьютекс перед Wait!
// 🤡 Джун напишет тут 'if'. Сеньор пишет 'for'. Почему — ниже. for !a.configLoad { // Wait атомарно снимает лок с a.mu и усыпляет горутину. // Когда она проснется, Wait атомарно вернет лок на место. a.cond.Wait() }
a.mu.Unlock() // Разлочили и пошли работать fmt.Printf("Воркер %d начал работу!\n", id) }
func (a *AppState) UpdateConfig() { a.mu.Lock() a.configLoad = true a.mu.Unlock()
// БУМ! Будим все тысячи спящих горутин одним вызовом a.cond.Broadcast() }
🔥 Senior Tip: Почему Wait() всегда пишут в цикле for?
Главная ловушка sync.Cond заключается в том, что вызов Wait() нельзя использовать внутри if !condition { cond.Wait() }. Спецификация POSIX-систем и рантайм Go допускают Spurious Wakeups (Ложные пробуждения). Горутина может проснуться просто так, даже если никто не вызывал Broadcast. Кроме того, между моментом, когда горутина проснулась, и моментом, когда она захватила мьютекс для проверки, другая горутина могла успеть поменять состояние configLoad обратно на false. Цикл for гарантирует, что проснувшаяся горутина обязательно еще раз проверит условие, прежде чем пойти дальше.
Когда sync.Cond НЕ нужен? Если событие происходит только один раз за время жизни приложения (например, Graceful Shutdown) - не тащите sync.Cond. Обычного закрытия канала close(done) или использования context.Context хватит за глаза. sync.Cond блистает именно в сценариях многоразовых массовых оповещений.
Сервис на проде внезапно упирается в полку по процессору.
Что делает новичок? Сидит и смотрит в исходники взглядом гипнотизера, пытаясь угадать: "Ну, наверное, это регулярка тормозит". Или обкладывает весь код вызовами time.Now() в начале и time.Since() в конце каждой функции.
Сеньор открывает терминал, пишет одну команду и через 30 секунд получает точное имя функции, номер строки и процент украденного CPU. Наш инструмент pprof.
Он встроен прямо в стандартную библиотеку Go. Работает по принципу семплирования: раз в 10 миллисекунд рантайм Go "замирает", смотрит на стеки всех запущенных горутин и записывает: "Так, в эту микросекунду процессор выполнял функцию json.Unmarshal.
Давайте проведем вскрытие за 4 шага.
Шаг 1. Включаем «жучка» в коде
Всё, что нужно сделать в вашем сервисе - импортировать один пакет со знаком подчеркивания:
func main() { // ... ваша основная бизнес-логика ...
// Вешаем pprof на отдельный внутренний порт! go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() }
Нюанс для Senior-ов: Магия _ "net/http/pprof" работает только с дефолтным http.DefaultServeMux. Если вы используете chi, gin или кастомный http.NewServeMux(), роуты профилировщика нужно регистрировать в ваш роутер руками (они лежат в пакете net/http/pprof как обычные хендлеры).
Шаг 2. Натравливаем профилировщик
Сервис крутится под нагрузкой. Открываем терминал на своей рабочей машине и говорим Go: *"Послушай этот сервер 30 секунд и собери мне статистику"*:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
Терминал подумает полминуты и превратится в интерактивную консоль (pprof).
Шаг 3. Магия двух колонок
Внутри консоли пишем команду top10:
Showing nodes accounting for 2.45s, 88.1% of 2.78s total flat flat% sum% cum cum% 1.82s 65.5% 65.5% 1.82s 65.5% regexp.(*bitState).reset 0.34s 12.2% 77.7% 0.45s 16.2% encoding/json.Unmarshal 0.15s 5.4% 83.1% 2.50s 89.9% main.processOrder Главная ловушка новичка - смотреть на колонку cum.
• flat - сколько времени процессор провел исключительно внутри тела этой функции. • cum (cumulative) - сколько времени процессор провел в этой функции + во всех функциях, которые она вызвала внутри себя.
Пример: У функции main.processOrder показатель cum равен 89.9%, но flat всего 5.4%. Это значит, что сама она почти ничего не считает, она просто вызвала тяжелую регулярку (regexp.reset, у которой flat аж 65.5%). Лечить нужно регулярку, а не processOrder!
Шаг 4. Визуальный экстаз (Flame Graph)
Смотреть в ASCII-таблицы в 2026 году больно. Выходим из консоли (Ctrl+D) и запускаем ту же команду, но добавив флаг -http:
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
В браузере мгновенно откроется шикарный веб-интерфейс. Переключаем вид сверху в режим Flame Graph (Огненный граф):
1. Перед вами "столбы". Чем шире столб по горизонтали - тем больше CPU сожрала эта функция. 2. Кликаем мышкой на самый широкий красный столб. 2. Переключаемся во вкладку Source. 3. Видим наш исходный код, где подсвечена конкретная строчка var re = regexp.MustCompile(...), компилирующая регулярку внутри цикла в хендлере. Прод спасен.
🔥 Senior Warning: Кровавая цена дефолта
Никогда не выставляйте порт 6060 наружу в интернет!
По адресу /debug/pprof/goroutine?debug=2 любой школьник без авторизации скачает полный текстовый дамп всех запущенных горутин. В их стеках будут лежать ваши пароли от БД в сыром виде, Bearer-токены пользователей и приватные ключи.
В продакшене pprof обязан висеть строго на localhost, куда разработчик заходит через kubectl port-forward или SSH-туннель.
🗺️ sync.Map: Почему эта «серебряная пуля» иногда пробивает дно производительности
Как только новичок ловит свою первую панику fatal error: concurrent map writes, он тут же гуглит проблему, находит sync.Map и радостно заменяет все свои словари на него. Казалось бы, идеальное решение из коробки.
Но если вы откроете исходники стандартной библиотеки Go, то увидите, что сами разработчики языка используют sync.Map крайне редко. В 95% случаев обычный map + sync.RWMutex порвет sync.Map по производительности.
Давайте разберем, как эта штука устроена под капотом и почему она может тормозить ваш сервис.
Архитектура двух корзин Внутри sync.Map лежат не одна, а две мапы:
1. read: Доступна для чтения вообще без блокировок. 2. dirty: Защищена классическим мьютексом.
Когда вы делаете Load(key), Go сначала бежит в read. Если ключ найден - супер, мы прочитали его lock-free. Но если ключа там нет, Go вынужден брать мьютекс и искать его в dirty мапе. Это называется cache miss. Если промахов становится слишком много, sync.Map принимает тяжелое решение: он берет всю dirty мапу и копирует её в read.
Когда sync.Map - это катастрофа?
• Частая запись новых ключей: Каждый новый ключ попадает в dirty. Это вызывает постоянные промахи при чтении, мьютекс постоянно лочится, а затем происходит дорогое копирование всей мапы. Вы получаете двойной удар по CPU. • Удаление и перезапись: Стандартный RWMutex справится с этим гораздо эффективнее.
Когда sync.Map - это шедевр? (Два идеальных юзкейса) Официальная документация выделяет ровно два сценария, где этот тип оправдан:
1. Append-only кэши: Когда записи добавляются один раз и живут вечно (например, кэш скомпилированных регулярных выражений или конфигураций). 2. Disjoint keys (Разделенные ключи): Когда у вас есть 1000 горутин, и каждая читает/пишет строго в свой собственный ключ, никогда не пересекаясь с соседями.
🔥 Senior Tip: Sharded Map (Шардирование) Что делать, если у вас кэш на 5 миллионов записей, куда постоянно идут конкурентные чтение и запись? RWMutex станет узким местом (будет блокировать всю мапу целиком). В высоконагруженном проде используют Шардирование. Вы создаете слайс из 256 обычных мап, у каждой из которых свой маленький мьютекс. Хэш от ключа определяет, в какую именно мапу пойдет запрос. Это снижает конкуренцию за лок в 256 раз! (Готовые решения: orcaman/concurrent-map или alphadose/haxmap).
Necessary cookies keep the site working and secure: sign-in, form protection, language, theme and others. Yandex Metrica analytics cookies and Yandex Advertising Network ad cookies are enabled only with your consent. By clicking “Accept all” you give consent to processing of cookie data; see the Personal Data Processing Policy for details.