Перейти к основному содержимому

Открытое интервью на Middle Go разработчика

· 19 мин. чтения

Сегодня мы разберём живое собеседование на позицию middle Go-разработчика, в ходе которого кандидат Андрей решал задачу на написание функции проверки IP-адресов по правилам CIDR (аналог работы firewall). Интервьюер Саша не только оценивал технические навыки — умение работать со строками, битовыми операциями и сетевыми концепциями, — но и давал подсказки, направлял ход мысли и обсуждал подходы к решению, демонстрируя, как на самом деле проходят технические интервью в крупных компаниях.

Вопрос 1. Расскажите о своём опыте работы и проектах, над которыми вы работали.

Таймкод: 00:08:34

Ответ собеседника: Неполный. Опыт в IT около 15 лет. Работал на разных проектах: начинал с языка C (модули ядра, системные приложения), затем Python около 4 лет, после чего перешёл на Go. Последний проект был связан с диспетчеризацией транспортных средств для агрохолдинга — планирование вывоза продукции с полей на заводы, по сути упрощённая версия задачи коммивояжёра. Сейчас работает разработчиком на Go в дочерней компании Ростелекома, находится на испытательном сроке.

Правильный ответ:

Ответ кандидата содержит общую информацию, но для позиции Go-разработчика он недостаточно детализирован. Рекомендуется структурировать рассказ следующим образом:

1. Общая хронология и эволюция стека

Кратко описать путь в IT, как это сделал кандидат. Важно подчеркнуть, что переход на Go был осознанным — обычно это связано с потребностями в высоконагруженных системах, микросервисной архитектуре или инфраструктурных задачах.

2. Детализация Go-проектов

Здесь нужно раскрыть конкретику:

  • Какую роль выполнял (разработчик, архитектор, лид)
  • Какой стек технологий использовал (фреймворки, базы данных, брокеры сообщений, оркестрация)
  • Какую архитектуру применяли (монолит, микросервисы, event-driven)
  • Какие метрики производительности были достигнуты

3. Пример детального описания проекта

Проект диспетчеризации транспорта — это интересный кейс. Его стоит раскрыть глубже:

Проект: Система планирования логистики для агрохолдинга

Задача: Автоматизация маршрутизации транспорта между полями и
заводами переработки. По сути — задача оптимизации маршрутов
(Vehicle Routing Problem) с ограничениями по времени, грузоподъёмности
и приоритетам.

Мой вклад:
- Разработал сервис расчёта маршрутов на Go
- Интегрировал с картографическими сервисами (OSRM/GraphHopper)
- Реализовал алгоритм оптимизации на основе генетического алгоритма
- Настроил обработку событий через Kafka для обновления статусов

Стек: Go 1.21, PostgreSQL, Redis, Kafka, gRPC, Kubernetes

Результаты:
- Сокращение времени планирования маршрутов с 4 часов до 15 минут
- Снижение транспортных расходов на 12%
- Система обрабатывает 500+ транспортных средств одновременно

4. Технические вызовы и решения

Рассказать о конкретных технических проблемах и как они решались:

  • Проблемы с производительностью и как оптимизировали
  • Как решали вопросы консистентности данных
  • Как обеспечивали отказоустойчивость

5. Текущая позиция и ожидания

Упомянуть, что сейчас работаете в дочерней компании Ростелекома, но важно добавить:

  • Какие задачи решаете сейчас
  • Что хотите развивать в профессиональном плане
  • Почему интересна именно эта вакансия

6. Рекомендации по структуре ответа

Формула STAR хорошо работает для описания проектов:

  • Situation — контекст и проблема
  • Task — конкретная задача
  • Action — что именно сделали
  • Result — измеримый результат

Такой структурированный ответ покажет глубину опыта, способность к системному мышлению и умение доносить техническую информацию — всё это критически важно для Go-разработчика высокого уровня.

Вопрос 2. Какой стек технологий использовался на последних проектах (базы данных, брокеры сообщений, система деплоя, оркестрация)?

Таймкод: 00:11:03

Ответ собеседника: Неполный. Система деплоя строилась на GitLab CI: манифест с запуском тестов, линтеров, сборкой и пушем артефактов в Nexus, затем Kubernetes разворачивал артефакты. Настройку Kubernetes делал DevOps-инженер. База данных — PostgreSQL. В качестве брокера сообщений был выбран NATS.

Правильный ответ:

Ответ кандидата покрывает базовые компоненты, но для полноты картины стоит расширить описание и добавить контекст принятых решений.

1. CI/CD Pipeline

Описано верно, но можно дополнить:

# Пример .gitlab-ci.yml для Go проекта
stages:
- test
- lint
- build
- deploy

variables:
GO_VERSION: "1.21"
DOCKER_REGISTRY: "nexus.company.ru"

unit-tests:
stage: test
image: golang:${GO_VERSION}
script:
- go test -race -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
coverage: '/total:\s+\(statements\)\s+(\d+.\d+%)/'

lint:
stage: lint
image: golangci/golangci-lint:latest
script:
- golangci-lint run --timeout 5m ./...

build:
stage: build
image: golang:${GO_VERSION}
script:
- CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o app ./cmd/server
artifacts:
paths:
- app

docker-push:
stage: build
image: docker:latest
script:
- docker build -t ${DOCKER_REGISTRY}/app:${CI_COMMIT_SHA} .
- docker push ${DOCKER_REGISTRY}/app:${CI_COMMIT_SHA}

2. PostgreSQL — детали использования

Стоит уточнить:

  • Версию PostgreSQL (14/15/16)
  • Использовались ли расширения (PostGIS для геоданных, pg_partman для партиционирования)
  • Как организованы миграции (goose, migrate, atlas)
  • Настройка пула соединений
// Пример настройки пула соединений в Go
import (
"github.com/jackc/pgx/v5/pgxpool"
)

func NewDBPool(dsn string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}

config.MaxConns = 50
config.MinConns = 10
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute

return pgxpool.NewWithConfig(context.Background(), config)
}

3. NATS — контекст выбора

NATS — хороший выбор для определённых сценариев. Стоит пояснить почему именно он:

  • NATS Core — легковесный, fire-and-forget, высокая пропускная способность
  • NATS JetStream — добавляет персистентность, exactly-once delivery
// Пример работы с NATS JetStream в Go
import (
"github.com/nats-io/nats.go"
)

func SetupJetStream() (nats.JetStreamContext, error) {
nc, err := nats.Connect("nats://nats-server:4222")
if err != nil {
return nil, err
}

js, err := nc.JetStream(nats.PublishAsyncMaxPending(256))
if err != nil {
return nil, err
}

// Создание потока
_, err = js.AddStream(&nats.StreamConfig{
Name: "ROUTES",
Subjects: []string{"routes.*"},
Storage: nats.FileStorage,
Retain: nats.LimitsPolicy,
MaxAge: 7 * 24 * time.Hour,
})

return js, err
}

4. Kubernetes — что стоит добавить

Хотя настройкой занимался DevOps, разработчику полезно знать:

  • Helm charts или Kustomize для управления манифестами
  • Health checks (liveness, readiness probes)
  • Resource limits и requests
  • HPA для автоскейлинга
// Health check endpoint для Kubernetes
func healthHandler(w http.ResponseWriter, r *http.Request) {
// Проверка зависимостей
if err := db.Ping(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}

w.WriteHeader(http.StatusOK)
}

5. Чего не хватает в ответе

Для полноты стоит упомянуть:

  • Мониторинг: Prometheus + Grafana, OpenTelemetry для трейсинг
  • Логирование: структурированные логи (zap, slog), централизованный сбор (ELK, Loki)
  • Service mesh: Istio/Linkerd если используется
  • Кэширование: Redis для кэширования данных
  • API Gateway: Kong, Traefik или самописное решение
  • Секреты: Vault, Sealed Secrets, External Secrets Operator

6. Как улучшить ответ

Вместо простого перечисления технологий, свяжите их с бизнес-задачами:

Выбор NATS обусловлен требованиями к низкой латентности
(маршруты должны пересчитываться в реальном времени) и
простотой эксплуатации. PostgreSQL с расширением PostGIS
позволил эффективно работать с географическими данными
для расчёта расстояний. Kubernetes обеспечил масштабируемость
в периоды пиковых нагрузок (сезон уборки урожая).

Такой подход показывает не просто знание инструментов, а понимание архитектурных решений и их обоснование.

Вопрос 3. Почему был выбран NATS в качестве брокера сообщений?

Таймкод: 00:12:54

Ответ собеседника: Неполный. Опыта с брокерами сообщений практически не было. Изучил, что есть нового на рынке, прочитал серию статей и тесты, вдохновился и решил применить NATS на практике. Впоследствии нашёл баг в NATS, который довольно быстро исправили.

Правильный ответ:

Ответ честный, но не содержит технического обоснования выбора. Для позиции Go-разработчика важно показать понимание критериев выбора брокера сообщений.

1. Критерии выбора брокера сообщений

При выборе брокера обычно оценивают:

  • Пропускную способность (messages per second)
  • Латентность (p50, p95, p99)
  • Гарантии доставки (at-most-once, at-least-once, exactly-once)
  • Персистентность сообщений
  • Модель маршрутизации (pub/sub, point-to-point, request-reply)
  • Простоту эксплуатации и мониторинга
  • Экосистему клиентов, интеграций

2. Сравнение популярных брокеров

┌─────────────────┬──────────┬──────────┬──────────┬──────────┐
│ Критерий │ NATS │ Kafka │ RabbitMQ │ Redis │
│ │ │ │ │ Streams │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Пропускная │ ★★★★★ │ ★★★★ │ ★★★ │ ★★★★ │
│ способность │ │ │ │ │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Латентность │ ★★★★★ │ ★★★ │ ★★★ │ ★★★★ │
│ (p99) │ <1ms │ 5-10ms │ 1-5ms │ 1-2ms │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Персистентность │ JetStream│ ★★★★★ │ ★★★★ │ ★★★ │
│ │ ★★★★ │ │ │ │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Сложность │ ★★★★★ │ ★★★ │ ★★★★ │ ★★★★ │
│ эксплуатации │ │ │ │ │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Маршрутизация │ ★★★★ │ ★★★ │ ★★★★★ │ ★★★ │
├─────────────────┼──────────┼──────────┼──────────┼──────────┤
│ Exactly-once │ JetStream│ ★★★★★ │ ★★★ │ ★★ │
└─────────────────┴──────────┴──────────┴──────────┴──────────┘

3. Почему NATS может быть хорошим выбором

A. Производительность

NATS Core — один из самых быстрых брокеров благодаря минималистичному протоколу:

// Бенчмарк публикации сообщений
func BenchmarkNATS_Publish(b *testing.B) {
nc, _ := nats.Connect(nats.DefaultURL)
defer nc.Close()

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
nc.Publish("bench.subject", []byte("payload"))
}
})
}
// Результат: ~2M msg/sec на одном узле

B. Простота развёртывания

Один бинарный файл ~15MB, минимальные зависимости:

# Запуск NATS в Docker
docker run -d --name nats -p 4222:4222 -p 8222:8222 nats:2.10 \
--jetstream \
--store_dir /data/jetstream \
--http_port 8222

C. Нативная интеграция с Go

NATS написан на Go, клиентская библиотека — официальная и хорошо поддерживается:

import "github.com/nats-io/nats.go"

// Request-Reply паттерн — нативно поддерживается
func handleRequest(msg *nats.Msg) {
response := process(msg.Data)
msg.Respond(response)
}

nc.Subscribe("route.calculate", handleRequest)

// Отправка запроса с таймаутом
resp, err := nc.Request("route.calculate", data, 5*time.Second)

4. Когда NATS — не лучший выбор

Стоит признать ограничения:

  • Kafka лучше подходит для event sourcing и обработки потоков с большими объёмами данных
  • RabbitMQ предпочтителен при сложной маршрутизации и корпоративных паттернах
  • Redis Streams удобен если Redis уже в стеке для кэширования

5. Как мог бы звучать полный ответ

Выбор NATS был обусловлен несколькими факторами:

1. Требования к латентности: маршруты должны пересчитываться
в реальном времени при изменении условий (поломка техники,
изменение приоритетов)

2. Простота эксплуатации: команда была небольшой, и хотелось
минимизировать операционные расходы

3. Модель использования: нам нужен был pub/sub для уведомлений
об изменениях статусов и request-reply для синхронных запросов
к сервису расчёта маршрутов

4. JetStream обеспечил персистентность критичных событий
(назначение маршрута транспорту) без необходимости поднимать
отдельный кластер Kafka

Для логирования и аудита мы дополнительно использовали
PostgreSQL, а NATS применялся только для операционных событий.

6. Факт нахождения бага

Это позитивный момент! Можно развить:

Нашёл баг в JetStream при работе с большими батчами сообщений
— при размере батча > 1000 сообщений наблюдался race condition
при подтверждении. Оформил issue с воспроизводящим тестом,
исправление вышло в следующем патче. Это показывает мой подход:
не просто использую инструменты, а углубляюсь в их работу
при возникновении проблем.

Такой ответ демонстрирует техническую зрелость, понимание торгов и способность обосновывать архитектурные решения.

Вопрос 4. Был ли опыт работы тимлидом или в основном занимались разработкой?

Таймкод: 00:13:39

Ответ собеседника: Правильный. Был опыт тимлидства, но соотношение примерно 20% тимлидство / менеджмент и 80% разработка. Помогал руководителю проекта с декомпозицией задач, но основная деятельность — разработка.

Правильный ответ:

Ответ кандидата реалистичный и честный. Соотношение 20/80 между менеджментом и разработкой — типичная ситуация для позиций вроде Tech Lead или Senior Developer с лидерскими функциями.

1. Типичные роли с подобным соотношением

  • Tech Lead — техническое лидерство без прямого управления людьми
  • Senior Developer — старший разработчик с менторскими функциями
  • Team Lead — лид команды с частичной загрузкой разработкой

2. Что может входить в 20% лидерских функций

A. Декомпозиция и оценка задач

Помощь в декомпозиции — это ценный навык. Пример подхода:

Эпик: Интеграция с картографическим сервисом
├── Исследование API картографических провайдеров (2 SP)
├── Абстракция для смены провайдера (3 SP)
├── Кэширование результатов геокодирования (5 SP)
├── Обработка rate limits (3 SP)
├── Graceful degradation при недоступности (5 SP)
└── Мониторинг и алертинг (3 SP)

B. Code Review

// Пример типичных замечаний при ревью:
// - Отсутствие контекста с таймаутом
// - Нет обработки ошибок
// - Можно использовать sync.Pool для аллокаций
// - Нужны юнит-тесты для edge cases

func ProcessRoutes(routes []Route) error {
// Проблема: нет контекста
for _, route := range routes {
if err := calculate(route); err != nil {
return err // Проблема: нет wrap с контекстом
}
}
return nil
}

// Исправленная версия
func ProcessRoutes(ctx context.Context, routes []Route) error {
for i, route := range routes {
if err := ctx.Err(); err != nil {
return fmt.Errorf("context cancelled: %w", err)
}
if err := calculate(ctx, route); err != nil {
return fmt.Errorf("route %d: %w", i, err)
}
}
return nil
}

C. Менторство младших разработчиков

  • Проведение парного программирования
  • Объяснение архитектурных решений
  • Помощь с отладкой сложных проблем

D. Технические решения

  • Выбор технологий и библиотек
  • Проектирование API
  • Определение стандартов кодирования

3. Как можно усилить ответ

Помимо декомпозиции задач, в мои лидерские функции входило:

1. Code Review — регулярно проверял код коллег,
обращая внимание на обработку ошибок и производительность

2. Технические решения — выбирал подходы к реализации
сложных компонентов (например, алгоритм оптимизации маршрутов)

3. Документирование — писал ADR (Architecture Decision Records)
для ключевых решений

4. Онбординг — помог адаптироваться двум новым разработчикам
в команде

При этом 80% времени уделял разработке: написание кода,
отладка, оптимизация производительности.

4. Почему такой баланс ценен

Для Go-разработчика с лидерскими функциями важно:

  • Оставаться «в коде» для поддержания технической экспертизы
  • Понимать командные динамики и уметь коммуницировать
  • Балансировать между идеальным решением и бизнес-дедлайнами

5. Рекомендация

Ответ кандидата полный и правильный. Для позиции Senior/Tech Lead такой баланс — это преимущество, а не недостаток. Стоит подчеркнуть, что желание оставаться в разработке показывает приверженность техническому развитию.

Вопрос 5. Напишите функцию, которая определяет, разрешён ли трафик для данного IP-адреса на основе списка правил в формате CIDR с указанием allow/deny (first-match wins).

Таймкод: 00:15:44

Ответ собеседника: Неполный. Кандидат начал обсуждать архитектуру решения: нужна функция, проверяющая включение IP в CIDR-блок. Если IP попадает в deny-список — возвращает false, если в allow — true. Обсуждал конвертацию IP-адреса в целое число (uint32), упомянул отсутствие стандартной библиотеки для этого в Go. Предлагал использовать регулярные выражения или ручной парсинг строки. Начал писать функцию конвертации IP в uint32 с побайтовым сдвигом и создание битовой маски через сдвиг (1 << (32 - maskBits)), но код не был завершён из-за ошибок в парсинге строки (проблемы с условиями разделителей и накопления символов). Алгоритм матчинга устно описал верно: применить битовую маску к IP и сравнить с адресом из правила, идти по списку правил с приоритетом первого совпадения.

Правильный ответ:

Задача на проверку принадлежности IP к CIDR-диапазону с приоритизацией правил. Кандидат верно описал алгоритм, но не смог реализовать его кодом.

1. Структура данных для правил

package firewall

import (
"fmt"
"net"
"strings"
)

// Action определяет действие для правила
type Action int

const (
ActionDeny Action = iota
ActionAllow
)

// Rule представляет одно правило firewall
type Rule struct {
CIDR string // например "192.168.1.0/24"
Action Action
}

// Firewall содержит список правил
type Firewall struct {
rules []Rule
}

2. Решение с использованием стандартной библиотеки Go

В Go есть пакет net, который умеет работать с CIDR:

// NewFirewall создаёт firewall из строковых правил
func NewFirewall(rules []string) (*Firewall, error) {
fw := &Firewall{
rules: make([]Rule, 0, len(rules)),
}

for _, r := range rules {
action, cidr, err := parseRule(r)
if err != nil {
return nil, fmt.Errorf("invalid rule %q: %w", r, err)
}

// Валидация CIDR
_, _, err = net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("invalid CIDR %q: %w", cidr, err)
}

fw.rules = append(fw.rules, Rule{
CIDR: cidr,
Action: action,
})
}

return fw, nil
}

// parseRule разбирает строку правила формата "allow 192.168.1.0/24"
func parseRule(rule string) (Action, string, error) {
parts := strings.SplitN(strings.TrimSpace(rule), " ", 2)
if len(parts) != 2 {
return ActionDeny, "", fmt.Errorf("expected format: action cidr")
}

var action Action
switch strings.ToLower(parts[0]) {
case "allow":
action = ActionAllow
case "deny":
action = ActionDeny
default:
return ActionDeny, "", fmt.Errorf("unknown action: %s", parts[0])
}

return action, parts[1], nil
}

// IsAllowed проверяет, разрешён ли трафик для данного IP
// Реализует стратегию first-match wins
func (fw *Firewall) IsAllowed(ipStr string) (bool, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return false, fmt.Errorf("invalid IP address: %s", ipStr)
}

// Конвертируем в IPv4 если это IPv4-mapped IPv6
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}

for _, rule := range fw.rules {
_, ipNet, err := net.ParseCIDR(rule.CIDR)
if err != nil {
return false, fmt.Errorf("invalid CIDR in rule: %w", err)
}

if ipNet.Contains(ip) {
// First match wins
return rule.Action == ActionAllow, nil
}
}

// По умолчанию запрещаем, если ни одно правило не совпало
return false, nil
}

3. Решение с битовыми операциями (для понимания)

Если хочется реализовать вручную:

// ipToUint32 конвертирует IPv4 строку в uint32
func ipToUint32(ipStr string) (uint32, error) {
parts := strings.Split(ipStr, ".")
if len(parts) != 4 {
return 0, fmt.Errorf("invalid IPv4 format")
}

var result uint32
for i, part := range parts {
var b uint32
_, err := fmt.Sscanf(part, "%d", &b)
if err != nil || b > 255 {
return 0, fmt.Errorf("invalid octet: %s", part)
}
result |= b << (24 - i*8)
}
return result, nil
}

// cidrToRange возвращает маску и сеть для CIDR
func cidrToRange(cidr string) (mask uint32, network uint32, err error) {
parts := strings.Split(cidr, "/")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid CIDR format")
}

network, err = ipToUint32(parts[0])
if err != nil {
return 0, 0, err
}

var prefixLen int
_, err = fmt.Sscanf(parts[1], "%d", &prefixLen)
if err != nil || prefixLen < 0 || prefixLen > 32 {
return 0, 0, fmt.Errorf("invalid prefix length")
}

if prefixLen == 0 {
mask = 0
} else {
mask = 0xFFFFFFFF << (32 - prefixLen)
}

network &= mask
return mask, network, nil
}

// contains проверяет, входит ли IP в CIDR
func contains(ipStr, cidr string) bool {
ip, err := ipToUint32(ipStr)
if err != nil {
return false
}

mask, network, err := cidrToRange(cidr)
if err != nil {
return false
}

return (ip & mask) == network
}

4. Тесты

package firewall

import (
"testing"
)

func TestFirewall_IsAllowed(t *testing.T) {
rules := []string{
"allow 10.0.0.0/8",
"deny 10.0.1.0/24",
"allow 10.0.1.100/32",
"deny 172.16.0.0/12",
"allow 192.168.0.0/16",
}

fw, err := NewFirewall(rules)
if err != nil {
t.Fatalf("failed to create firewall: %v", err)
}

tests := []struct {
ip string
allowed bool
desc string
}{
{"10.0.0.1", true, "matches allow 10.0.0.0/8"},
{"10.0.1.1", false, "matches deny 10.0.1.0/24 before allow /8"},
{"10.0.1.100", true, "matches allow 10.0.1.100/32 before deny /24"},
{"172.16.0.1", false, "matches deny 172.16.0.0/12"},
{"192.168.1.1", true, "matches allow 192.168.0.0/16"},
{"8.8.8.8", false, "no match, default deny"},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := fw.IsAllowed(tt.ip)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.allowed {
t.Errorf("IsAllowed(%s) = %v, want %v", tt.ip, got, tt.allowed)
}
})
}
}

5. Ключевые моменты решения

  • Используйте net.ParseCIDR и net.IPNet.Contains — стандартная библиотека Go уже содержит нужный функционал
  • First-match wins — итерация по правилам в порядке приоритета
  • Default deny — безопасная практика: если ни одно правило не совпало, запрещаем
  • Валидация входных данных — всегда проверяйте формат IP и CIDR

6. Типичные ошибки

  • Забывают про To4() для IPv4-mapped IPv6 адресов
  • Неправильно вычисляют маску: 0xFFFFFFFF << (32 - prefixLen), а не 1 << (32 - prefixLen)
  • Не обрабатывают граничные случаи /0 и /32
  • Используют net.ParseIP без проверки на nil

Вопрос 6. Что такое CIDR-запись (например, 192.168.1.0/24) и как она работает?

Таймкод: 00:17:15

Ответ собеседника: Правильный. CIDR-запись состоит из IP-адреса и количества значимых бит маски подсети. IP-адрес — это 32 бита, разделённые точками на 4 октета. Число после слэша указывает, сколько первых бит должны совпадать точно. Например, /24 означает, что первые 24 бита (первые 3 октета) фиксированы, а последние 8 бит (последний октет) могут быть произвольными — от 0 до 255. Таким образом 192.168.1.0/24 описывает диапазон 192.168.1.0–192.168.1.255.

Правильный ответ:

Ответ кандидата корректный и покрывает основы. Для полноты можно дополнить техническими деталями и примерами использования.

1. Определение CIDR

CIDR (Classless Inter-Domain Routing) — это метод адресации и маршрутизации IP-пакетов, который заменил классовую адресацию (A, B, C). CIDR позволяет гибко определять размер подсети.

2. Структура CIDR-записи

192.168.1.0/24
│ │ │
│ │ └── Длина префикса (количество бит сети)
│ └──── Разделитель
└──────────────── Адрес сети (base address)

3. Битовое представление

IP: 192.168.1.0
Двоичное: 11000000.10101000.00000001.00000000

Маска /24:
Двоичное: 11111111.11111111.11111111.00000000
Десятичное: 255.255.255.0

Биты сети: 11000000.10101000.00000001 (24 бита)
Биты хоста: 00000000 (8 бит)

4. Математика CIDR

// Вычисление параметров подсети из CIDR
func calculateSubnet(cidr string) {
// Для 192.168.1.0/24:
prefixLen := 24

// Маска подсети
mask := uint32(0xFFFFFFFF) << (32 - prefixLen)
// mask = 0xFFFFFF00 = 255.255.255.0

// Количество адресов в подсети
hostBits := 32 - prefixLen
numAddresses := 1 << hostBits
// numAddresses = 256

// Broadcast адрес
network := baseIP & mask
broadcast := network | ^mask
// broadcast = 192.168.1.255

// Первый и последний используемые адреса
firstUsable := network + 1 // 192.168.1.1
lastUsable := broadcast - 1 // 192.168.1.254
}

5. Типичные маски подсетей

┌────────┬─────────────────┬──────────────┬───────────────┐
│ Префикс│ Маска │ Хостов │ Типичное │
│ │ │ │ использование │
├────────┼─────────────────┼──────────────┼───────────────┤
│ /32 │ 255.255.255.255 │ 1 │ Один хост │
│ /31 │ 255.255.255.254 │ 2 │ Point-to-point│
│ /30 │ 255.255.255.252 │ 2 │ WAN линк │
│ /24 │ 255.255.255.0 │ 254 │ Малая сеть │
│ /16 │ 255.255.0.0 │ 65534 │ Большая сеть │
│ /8 │ 255.0.0.0 │ 16777214 │ Очень большая │
│ /0 │ 0.0.0.0 │ 4294967294 │ Все адреса │
└────────┴─────────────────┴──────────────┴───────────────┘

6. Проверка вхождения IP в CIDR

func ipInCIDR(ip, cidr string) bool {
parsedIP := net.ParseIP(ip)
_, ipNet, _ := net.ParseCIDR(cidr)
return ipNet.Contains(parsedIP)
}

// Примеры:
ipInCIDR("192.168.1.100", "192.168.1.0/24") // true
ipInCIDR("192.168.2.1", "192.168.1.0/24") // false
ipInCIDR("10.0.0.1", "10.0.0.0/8") // true

7. Агрегация маршрутов

CIDR позволяет объединять маршруты:

До агрегации:
192.168.0.0/24
192.168.1.0/24
192.168.2.0/24
192.168.3.0/24

После агрегации:
192.168.0.0/22 (объединяет 4 подсети /24)

8. Private адреса по RFC 1918

10.0.0.0/8 — 16,777,216 адресов
172.16.0.0/12 — 1,048,576 адресов
192.168.0.0/16 — 65,536 адресов

Ответ кандидата полностью корректен для базового понимания CIDR.

Вопрос 7. Когда следует использовать брокер сообщений (например, Kafka), а можно обойтись без него?

Таймкод: 01:32:10

Ответ собеседника: Правильный. Брокер сообщений нужен, когда требуется буферизация (асинхронная обработка при большом потоке входящих данных и малой пропускной способности обработки), семантика очереди (гарантия доставки, возможность забирать задачи по мере готовности), распределение нагрузки между репликами (каждая реплика берёт индивидуальную задачу из очереди), а также когда нужна устойчивость к недоступности сервисов. Прямые синхронные вызовы (gRPC) предполагают, что сервис всегда доступен, тогда как очередь снимает эту проблему через буфер.

Правильный ответ:

Ответ кандидата покрывает основные причины использования брокера сообщений. Дополним структурированным сравнением и конкретными сценариями.

1. Когда нужен брокер сообщений

A. Асинхронная обработка и буферизация

Сценарий: Пиковая нагрузка превышает возможности обработки

Без брокера:
Клиент → [Сервис A] → [Сервис B] → Ответ

Если B перегружен — ошибка

С брокером:
Клиент → [Сервис A] → [Брокер] → [Сервис B] → Ответ
↑ ↑
Быстрый ответ Буфер накапливает

B. Гарантии доставки

At-most-once: Сообщение может быть потеряно (fire-and-forget)
At-least-once: Сообщение доставится, возможно дублирование
Exactly-once: Сообщение доставится ровно один раз (сложно реализовать)

C. Декорреляция сервисов

// Без брокера — жёсткая связанность
func (s *OrderService) CreateOrder(ctx context.Context, order Order) error {
// Нужно знать ВСЕ зависимые сервисы
if err := s.inventory.Reserve(ctx, order.Items); err != nil {
return err
}
if err := s.payment.Charge(ctx, order.Payment); err != nil {
return err
}
if err := s.notification.SendConfirmation(ctx, order.UserID); err != nil {
return err
}
// Добавление нового сервиса = изменение этого кода
return nil
}

// С брокером — слабая связанность
func (s *OrderService) CreateOrder(ctx context.Context, order Order) error {
// Публикуем событие, не зная кто его обработает
event := OrderCreatedEvent{Order: order}
return s.publisher.Publish(ctx, "orders.created", event)
// Новые подписчики добавляются без изменения этого кода
}

2. Когда можно обойтись без брокера

A. Простые CRUD операции

// Синхронный REST/gRPC вызов вполне достаточен
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.db.GetUser(ctx, id)
}

B. Низкая нагрузка и простая архитектура

Микросервисов < 5
Нагрузка < 100 RPS
Нет требований к асинхронности

C. Когда нужен немедленный ответ

// Авторизация — нужен синхронный ответ
func (s *AuthService) Login(ctx context.Context, creds Credentials) (*Token, error) {
user, err := s.validateCredentials(ctx, creds)
if err != nil {
return nil, err
}
return s.generateToken(user), nil
}

3. Сравнение подходов

┌────────────────────┬──────────────────┬──────────────────┐
│ Критерий │ Без брокера │ С брокером │
├────────────────────┼──────────────────┼──────────────────┤
│ Сложность │ Низкая │ Высокая │
│ Латентность │ Низкая │ Выше │
│ Надёжность │ Зависит от │ Высокая │
│ │ доступности │ │
│ Масштабируемость │ Ограничена │ Хорошая │
│ Отладка │ Простая │ Сложнее │
│ Мониторинг │ Стандартный │ Нужен отдельный │
│ │ │ │
│ Eventual │ Нет │ Да │
│ consistency │ │ │
└────────────────────┴──────────────────┴──────────────────┘

4. Альтернативы брокеру для простых случаев

A. Встроенные каналы Go

// Для внутрипроцессной очереди
type WorkerPool struct {
jobs chan Job
}

func NewWorkerPool(workers int) *WorkerPool {
pool := &WorkerPool{
jobs: make(chan Job, 1000), // Буфер
}
for i := 0; i < workers; i++ {
go pool.worker()
}
return pool
}

B. База данных как очередь

-- Паттерн "transactional outbox"
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id VARCHAR NOT NULL,
event_type VARCHAR NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
processed BOOLEAN DEFAULT FALSE
);

-- Получение необработанных событий
SELECT * FROM outbox
WHERE processed = FALSE
ORDER BY created_at
LIMIT 100
FOR UPDATE SKIP LOCKED;

C. Redis Streams

// Лёгкая альтернатива для простых случаев
func (s *Service) PublishEvent(ctx context.Context, event Event) error {
return s.redis.XAdd(ctx, &redis.XAddArgs{
Stream: "events",
Values: map[string]interface{}{
"type": event.Type,
"data": event.Data,
},
}).Err()
}

5. Рекомендации по выбору

Используйте брокер когда:
✓ Нужна гарантия доставки сообщений
✓ Пиковая нагрузка значительно превышает среднюю
✓ Сервисы могут быть недоступны
✓ Нужна история событий (event sourcing)
✓ Много подписчиков на события

Обойдитесь без брокера когда:
✓ Простая синхронная коммуникация
✓ Низкая и стабильная нагрузка
✓ Мало сервисов (< 5)
✓ Нужен немедленный ответ
✓ Команда небольшая и нет опыта с брокерами

Ответ кандидата полный и демонстрирует понимание торгов при выборе архитектурных решений.