Все специализации

Технический писатель: вопросы на собеседовании

28 вопросов с разбором ответов — те формулировки, которые действительно встречаются на интервью.

Я технический писатель с многолетним опытом создания и поддержки документации для сложных IT-продуктов. Моя специализация — перевод технических требований и архитектурных решений в понятные и структурированные материалы для разработчиков и конечных пользователей.

Работал с разнообразными командами, включая разработчиков, QA и менеджеров проектов, чтобы обеспечить актуальность и полноту документации. Владею инструментами для управления контентом и автоматизации публикаций.

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

Микросервис может не масштабироваться по нескольким причинам:

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

Состояние (statefulness): микросервисы, которые хранят состояние локально, сложнее масштабировать горизонтально, так как требуется синхронизация состояния.

Узкие места в инфраструктуре: недостаточная производительность базы данных, сети или других внешних сервисов может ограничивать масштабирование.

Неправильная конфигурация: отсутствие автоматического масштабирования, лимиты ресурсов (CPU, память) или ошибки в настройках оркестратора.

Монолитные зависимости: если микросервис содержит монолитные компоненты или библиотеки, которые не поддерживают параллельную работу.

Для успешного масштабирования важно проектировать микросервисы как stateless, использовать эффективные механизмы кэширования, распределённые базы данных и правильно настраивать инфраструктуру.

Выбор структуры проекта на Go зависит от масштаба и целей проекта, но обычно ориентируюсь на следующие принципы:

Простота и понятность: структура должна быть интуитивной для новых участников.

Разделение по пакетам: логически связанные функции и типы группируются в отдельные пакеты.

Использование стандартных подходов, например, структура с папками cmd/, pkg/, internal/.

Пример базовой структуры:

Для больших проектов стоит рассмотреть архитектурные паттерны (например, Clean Architecture) и разделять бизнес-логику, интерфейсы и инфраструктуру.

Для локализации проблемы медленного потребления памяти сервисом следует:

Собрать метрики использования памяти с помощью профилировщиков (например, Valgrind massif, VisualVM, pprof) или встроенных инструментов мониторинга.

Проанализировать логи и трассировки на предмет утечек памяти или аномального роста объектов.

Использовать heap dump (дамп кучи) для анализа объектов, которые занимают много памяти.

Проверить циклы и кэширование — возможно, объекты не освобождаются из-за неправильного управления ссылками.

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

Например, в Java можно использовать VisualVM для мониторинга и создания heap dump, а затем анализировать, какие объекты накапливаются.

В Java:

Объяснение:

Java кэширует объекты Integer в диапазоне от -128 до 127. Поэтому a и b ссылаются на один и тот же объект, и сравнение == возвращает true. Для значений вне этого диапазона создаются новые объекты, поэтому c == d — false.

В Go:

Вывод будет:

Объяснение:

В Go функции, отложенные с помощью defer, захватывают переменные по ссылке, а не по значению. Поэтому при выполнении отложенной функции значение x уже равно 2.

Для реализации системы оплаты с помощью мобильного приложения, не используя СБП, можно предложить следующую архитектуру:

Идентификация пользователя на кассе/КСО:

Касса/КСО генерирует уникальный QR-код с информацией о покупке и идентификатором транзакции.

Пользователь сканирует QR-код в приложении Магнит.

Переход в приложение Магнит:

Приложение Магнит получает данные о покупке и предлагает выбрать банк для оплаты.

Оплата через банк:

Приложение Магнит инициирует переход в приложение банка с передачей параметров оплаты (сумма, идентификатор транзакции).

Пользователь подтверждает оплату в приложении банка.

Оплата через банк:

Подтверждение оплаты:

Банк отправляет уведомление о статусе оплаты в backend Магнита.

Backend Магнита обновляет статус транзакции.

Касса/КСО получает уведомление о успешной оплате через push-сообщение или polling.

Отображение результата:

Приложение Магнит показывает результат оплаты.

Касса/КСО завершает процесс покупки.

Нефункциональные требования:

Для обеспечения DAU 1000 на магазин и 500 магазинов с 5 кассами каждая, система должна быть масштабируемой и распределённой.

Использовать кэширование и быстрые очереди сообщений для минимизации задержек.

Оптимизировать генерацию и обработку QR-кодов.

Страница оплаты должна загружаться не более 250 мс — использовать CDN, минимизировать запросы.

Время оплаты до 2 минут — предусмотреть асинхронные уведомления и таймауты.

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

При работе с highload (высокой нагрузкой) баланс между количеством аллокаций и читаемостью кода зависит от конкретной ситуации, но обычно приоритет отдается минимизации аллокаций, так как они напрямую влияют на производительность и потребление памяти.

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

Оптимальный подход:

Профилировать приложение и выявлять узкие места.

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

Сохранять читаемость и понятность кода, используя понятные конструкции и комментарии.

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

Выбор между SQL и NoSQL зависит от требований проекта:

SQL (реляционные базы данных):

Подходят, когда важна строгая структура данных и сложные связи между сущностями.

Требуется поддержка транзакций с ACID-свойствами.

Хорошо подходят для аналитики и отчетности.

NoSQL (документные, ключ-значение, графовые и др.):

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

Подходят для больших объемов неструктурированных данных.

Могут жертвовать консистентностью ради производительности (BASE).

Миграции в MongoDB:

Хотя MongoDB не требует строгой схемы, миграции данных всё равно нужны при изменении структуры документов, например:

Добавление новых полей с дефолтными значениями.

Изменение формата хранения данных.

Удаление устаревших полей.

Для миграций в MongoDB часто используют скрипты на JavaScript или специализированные инструменты (например, Mongock), которые последовательно обновляют документы в коллекциях.

Таким образом, миграции нужны, но они менее формализованы, чем в реляционных базах.

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

Например, если у нас есть интерфейс Животное с методом издатьЗвук(), то любой класс, который реализует этот интерфейс (например, Собака или Кошка), должен будет написать, как именно он издаёт звук.

Это помогает писать код, который работает с разными объектами через общий интерфейс, не заботясь о деталях реализации.

В Java immutable объекты — это объекты, состояние которых нельзя изменить после создания. Классический пример — класс String. Другие известные immutable объекты:

Обёртки примитивных типов: Integer, Long, Double, Boolean и т.д.

Классы из пакета java.time (например, LocalDate, LocalDateTime, Instant), которые представляют неизменяемые даты и время.

Чтобы сделать собственный класс immutable, нужно:

Сделать класс final или не предоставлять методы для изменения состояния.

Все поля — private final.

Не предоставлять сеттеры.

Если поля — ссылки на изменяемые объекты, создавать их копии при передаче и возврате.

Пример простого immutable класса:

В моей последней компании интересным кейсом был переход на новую систему документации, которая должна была объединить внутренние и внешние материалы. Основной bottleneck возник вокруг согласования терминологии и структуры контента между разными командами — разработчиками, маркетингом и поддержкой.

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

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

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

Оркестрация и хореография — два подхода к управлению такими транзакциями:

Оркестрация — централизованный контроллер (оркестратор) управляет последовательностью действий во всех сервисах. Он отправляет команды и ожидает ответов, контролируя весь процесс.

Хореография — каждый сервис самостоятельно реагирует на события и выполняет свои действия без центрального контроллера. Сервисы обмениваются событиями и реагируют на них, обеспечивая согласованность через взаимодействие.

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

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

Для решения задач валидации данных, создания объекта и выбора платежного метода часто применяются следующие паттерны проектирования:

Валидация данных — Паттерн Chain of Responsibility

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

Создание объекта — Паттерн Builder

Удобен для поэтапного создания сложных объектов с множеством параметров, обеспечивая читаемость и гибкость.

Выбор платежного метода — Паттерн Strategy

Позволяет инкапсулировать различные алгоритмы оплаты и выбирать нужный в зависимости от условий (например, тип карты, регион, валюта).

Пример на псевдокоде для выбора платежного метода:

Такой подход облегчает поддержку и расширение функционала.

Для проверки целостности данных в webhook обычно используют цифровые подписи или HMAC (Hash-based Message Authentication Code). Отправитель вычисляет хеш сообщения с секретным ключом и передаёт его в заголовке. Получатель повторно вычисляет HMAC и сравнивает с полученным значением — если совпадает, данные не были изменены.

Для защиты данных на уровне транспорта применяют протокол HTTPS (TLS), который обеспечивает шифрование канала и предотвращает перехват или подмену данных.

Пример проверки HMAC на стороне получателя (на Python):

Таким образом, целостность и безопасность обеспечиваются комбинацией криптографической проверки и защищённого канала передачи.

Состояние гонки (race condition) — это ситуация в многопоточных или распределённых системах, когда результат работы программы зависит от непредсказуемого порядка выполнения операций. Обычно возникает при одновременном доступе нескольких потоков или процессов к общим ресурсам без должной синхронизации.

Например, если два потока одновременно пытаются увеличить значение общего счётчика, и операции чтения и записи не защищены, итоговое значение может быть некорректным. Это приводит к ошибкам, трудноуловимым багам и нестабильному поведению программы.

В продакшене с Go сталкивался с рядом проблем:

Управление зависимостями и версиями. До появления Go Modules были сложности с контролем версий библиотек.

Отсутствие generics (до Go 1.18). Это приводило к дублированию кода или использованию интерфейсов с потерей типобезопасности.

Проблемы с производительностью GC при большом количестве объектов. Иногда приходилось оптимизировать выделение памяти.

Ошибки в конкурентном коде. Несмотря на удобство goroutine, гонки данных и дедлоки требуют внимательного тестирования.

Ограниченная поддержка GUI и десктопных приложений. Go больше ориентирован на серверную часть.

Пример: при работе с большим числом соединений иногда наблюдались задержки из-за сборщика мусора, что решалось оптимизацией аллокаций и использованием пулов объектов.

Для реализации системы оплаты через мобильное приложение без использования СБП можно предложить следующую архитектуру:

Идентификация пользователя на кассе/КСО

Пользователь сканирует товары.

При переходе к оплате выбирает оплату через приложение Магнит.

Касса/КСО предлагает пользователю авторизоваться, например, через QR-код, NFC или ввод кода, который связывает кассу и мобильное приложение.

Запуск процесса оплаты

После идентификации касса передает в приложение Магнит информацию о сумме и деталях покупки.

Приложение Магнит открывается на телефоне пользователя (через deep link или push-уведомление).

Пользователь выбирает банк из списка установленных банковских приложений.

Оплата через банк

Приложение Магнит передает данные в выбранное банковское приложение через стандартный протокол (например, через Intent на Android или URL-схему на iOS).

Пользователь подтверждает оплату в банковском приложении.

Оплата через банк

Подтверждение оплаты

Банк отправляет подтверждение об успешной оплате обратно в приложение Магнит.

Приложение Магнит уведомляет кассу/КСО о результате оплаты через серверную часть.

Касса/КСО отображает сообщение об успешной оплате.

Нефункциональные требования и их обеспечение:

DAU = 1000 в магазине, 500 магазинов, 5 касс в среднем:

Серверная инфраструктура должна быть масштабируемой, использовать балансировщики нагрузки и кэширование.

Оплата за максимум 2 минуты:

Минимизировать задержки на каждом этапе, использовать быстрые протоколы обмена.

Страница оплаты открывается максимум за 250 мс:

Использовать deep linking, предварительную загрузку данных в приложении Магнит.

Пример взаимодействия:

Пользователь на кассе сканирует товары.

Касса генерирует QR-код с информацией о покупке и сессии.

Пользователь сканирует QR-код в приложении Магнит.

Приложение Магнит показывает сумму и предлагает выбрать банк.

После выбора банка происходит переход в банковское приложение для оплаты.

После успешной оплаты возвращается подтверждение, касса получает сигнал и завершает транзакцию.

Иногда я сознательно избегал использования горутин, даже если технически мог их применить. Основные причины:

Простота и читаемость кода. Если задача простая и не требует параллельного выполнения, добавление горутин может усложнить понимание и отладку.

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

Опасения по поводу гонок данных. Если код не защищён должным образом, использование горутин может привести к трудноуловимым багам.

Например, в небольшом CLI-утилите, где операции выполняются быстро и последовательно, я предпочитаю писать простой последовательный код без горутин, чтобы избежать лишней сложности.

В типичном приложении можно выделить несколько слоёв:

Презентационный слой (UI/Frontend) — взаимодействует с пользователем, отображает данные и принимает ввод.

Слой бизнес-логики (Backend) — реализует правила и процессы приложения.

Слой доступа к данным (DAL) — отвечает за взаимодействие с базой данных или другими хранилищами.

Между слоями обычно происходит общение через чётко определённые интерфейсы или API. Например:

UI вызывает методы бизнес-логики через REST API или RPC.

Бизнес-логика обращается к DAL через абстракции, скрывающие детали хранения.

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

Для безопасной передачи данных от формы к серверу используется протокол HTTPS, который обеспечивает шифрование и защиту от перехвата.

Как работает HTTPS:

Клиент (браузер) устанавливает соединение с сервером по протоколу TLS (Transport Layer Security).

Происходит TLS-рукопожатие, в ходе которого:

Клиент проверяет сертификат сервера (подписан ли он доверенным центром сертификации).

Стороны договариваются о параметрах шифрования.

Генерируется общий секретный ключ для сессии.

После установления защищённого канала все данные, включая содержимое формы, шифруются с помощью этого ключа и передаются по сети.

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

Для дополнительной безопасности стоит:

Использовать современные версии TLS (1.2 и выше).

Проверять корректность сертификатов.

Применять защиту от CSRF и XSS на стороне приложения.

В Java исключения (exceptions) делятся на несколько видов:

Checked exceptions (проверяемые исключения)

Наследуются от класса Exception, но не от RuntimeException.

Компилятор требует обязательной обработки (try-catch) или объявления в сигнатуре метода (throws).

Используются для ошибок, которые можно предвидеть и обработать, например, IOException, SQLException.

Unchecked exceptions (непроверяемые исключения)

Наследуются от RuntimeException.

Компилятор не требует обязательной обработки.

Обычно указывают на ошибки программирования, например, NullPointerException, IllegalArgumentException.

Errors (ошибки)

Наследуются от класса Error.

Представляют серьезные проблемы, которые обычно не обрабатываются приложением, например, OutOfMemoryError, StackOverflowError.

Errors (ошибки)

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

В Java при сравнении объектов типа Integer с помощью == сравниваются ссылки, а не значения. Однако для значений в диапазоне от -128 до 127 существует кэширование объектов (Integer Cache).

В вашем примере:

a и b равны 127, оба ссылаются на один и тот же объект из кэша, поэтому a == b будет true.

c и d равны 128, выходят за пределы кэша, поэтому создаются разные объекты, и c == d будет false.

Если нужно сравнить значения, следует использовать метод .equals():

Для построения сервиса я ориентируюсь на принципы 12-factor app, которые помогают создавать масштабируемые, поддерживаемые и переносимые приложения. Основные факторы включают:

Codebase — один репозиторий для кода, множество деплоев.

Dependencies — явное объявление и изоляция зависимостей.

Config — конфигурация хранится вне кода, например, в переменных окружения.

Backing services — сервисы (БД, очереди) считаются подключаемыми ресурсами.

Build, release, run — четкое разделение стадий сборки, релиза и запуска.

Processes — приложение запускается как один или несколько безсостоящих процессов.

Port binding — сервис экспортирует функциональность через порт.

Concurrency — масштабирование через модель процессов.

Disposability — быстрый старт и корректное завершение процессов.

Dev/prod parity — минимальная разница между средами разработки и продакшена.

Logs — логи считаются потоком событий и не управляются самим приложением.

Admin processes — административные задачи запускаются как одноразовые процессы.

Эти принципы помогают строить надежные и гибкие сервисы, легко адаптирующиеся к изменениям и нагрузкам.

Для безопасности API важно обеспечить аутентификацию, авторизацию, защиту от атак и шифрование данных.

Авторизация клиента:

Использование OAuth 2.0 с токенами доступа (Bearer tokens).

JWT (JSON Web Tokens) для передачи информации о пользователе и правах.

API-ключи с ограничениями по правам и IP.

Host-to-host авторизация:

Использование mTLS (Mutual TLS) — взаимная аутентификация по сертификатам.

IP-фильтрация и VPN для ограничения доступа.

Использование HMAC-подписей для проверки целостности и подлинности запросов.

Дополнительно:

HTTPS для шифрования трафика.

Ограничение количества запросов (rate limiting).

Логирование и мониторинг подозрительной активности.

Пример использования mTLS для host-to-host:

Сервер и клиент имеют свои сертификаты.

При установлении TLS-соединения оба проверяют сертификаты друг друга.

Только доверенные хосты могут установить соединение.

Таким образом, для клиентов обычно применяют токены и ключи, а для host-to-host — сертификаты и защищённые каналы.

Go — очень популярный язык, но у него есть свои минусы:

Отсутствие обобщений (generics) в ранних версиях усложняло написание универсального кода. Хотя с Go 1.18 generics появились, они пока не так гибки, как в других языках.

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

Минималистичный стандартный набор библиотек. Иногда приходится использовать сторонние библиотеки для распространённых задач.

Отсутствие исключений. Ошибки обрабатываются явно, что увеличивает количество кода и требует дисциплины.

Меньше возможностей для метапрограммирования. Нет макросов и рефлексии ограничена.

Эти особенности делают Go простым и предсказуемым, но иногда ограничивают гибкость и выразительность кода.

DTO (Data Transfer Object) используется для передачи данных между слоями приложения, например, между слоем бизнес-логики и слоем представления или между клиентом и сервером.

Основные причины использования DTO:

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

Оптимизация передачи данных: DTO содержит только необходимые поля, уменьшая объем передаваемых данных.

Безопасность: скрывает чувствительные или ненужные поля, которые не должны быть доступны на других слоях.

Упрощение сериализации: DTO часто проще сериализовать и десериализовать, так как они не содержат сложной логики.

Пример: в backend-сервисе есть сложная модель User с множеством полей и связей, но для передачи клиенту используется UserDTO с ограниченным набором полей (имя, email), что упрощает и защищает обмен данными.

В Go подсчёт количества символов в строке зависит от того, что считать символом — байтом, руном (Unicode code point) или графемой (user-perceived character).

Подсчёт байтов:

Подсчёт рун (Unicode code points):

Подсчёт графем (графемных кластеров):

Для точного подсчёта пользовательских символов (например, учитывая сложные эмодзи или комбинированные символы) нужно использовать сторонние библиотеки, например, github.com/rivo/uniseg:

Таким образом, выбор метода зависит от задачи: для простых случаев достаточно utf8.RuneCountInString, для точного подсчёта пользовательских символов — графемы.

В Go для подсчёта количества символов (рун) в строке нужно учитывать, что строка — это последовательность байтов в UTF-8, а руны — это Unicode-кодовые точки. Количество байтов и количество рун может отличаться.

Чтобы посчитать количество рун, можно использовать функцию utf8.RuneCountInString из пакета unicode/utf8:

Этот способ корректно считает количество символов, включая многобайтовые руны.

А если вопрос прозвучит не так, как вы готовились?

Так бывает чаще всего. ИзиСобес слышит вопрос интервьюера и подсказывает ответ прямо во время разговора — его не видно ни в Zoom, ни при демонстрации экрана.

Посмотреть ИзиСобес