Ниже — разбор вопросов с моего прошедшего ранее собеседования в Avito.Tech, на должность веб-разработчика php/go. Как известно, Авито — крупная ИТ-компания, входящая в условный “российский FAANG” или топ Тех-компаний ИТ отрасли. Попасть сюда сложно, отбор кандидатов долгий и многоступенчатый. Поэтому успешно пройти все этапы интервью сюда — это большой успех и показатель хорошей технической подготовки кандидата. Что ж, ниже мы разберем вопросики, на которых “сыпятся“ большинство из кандидатов.
/**/
- Вопросы
- Есть ли опыт работы с Golang ?
- Каково внутреннее устройство массивов в php ? Как растет память при передаче массива в ф-ю ?
- Как выделяется память под массив, передаваемый в ф-ю в качестве аргумента?
- Что нового в последних версиях php?
- В чем разница между jit и opcache
- Что такое zend vm
- В чем разница между генератором и итератором?
- Как работает сборщик мусора в phpM
- Что такое z_val ?
- Как добиться единого code-style в команде разработчиков?
- Как отловить ошибки в коде , которые не видит линтер?
- Как обеспечить тестирование в БД ?
- В phpunit есть моки?
- Задачи
Вопросы
Есть ли опыт работы с Golang ?
-
Да, прохожу курс Stepik по данному языку, вот примеры исходников решенных задач: https://bitbucket.org/mishaikon1/goexp/src/master/
-
Также решаю задачки по программированию на php, примеры тут: https://github.com/nujensait/phpexp/tree/main/CodeTasks
Каково внутреннее устройство массивов в php ? Как растет память при передаче массива в ф-ю ?
В PHP массивы реализованы как упорядоченные хеш-таблицы, где ключи могут быть любого типа данных, кроме ресурсов и объектов. Внутри массива каждый элемент хранится в виде пары ключ-значение. Ключи хранятся в виде хеш-значений, что позволяет быстро находить элементы в массиве. Значения могут быть любого типа данных, включая другие массивы и объекты.
Массивы в PHP также поддерживают автоматическое расширение при добавлении новых элементов и автоматическое сжатие при удалении элементов. Устройство в виде хеш-таблицы позволяет находить в них данные по ключу за единичное время О(1).
Интересный факт (по ссылке ниже): С ростом размера массива, время вставки в него и объем потребления памяти увеличиваются нелинейно.
Дело в том, что на уровне C (да и вообще на системном уровне), не бывает массивов, с нефиксированным размером. Каждый раз, когда вы создаете массив в C, вы должны указать его размер, чтобы система знала, сколько нужно памяти на него выделить.
Тогда как это реализовано в PHP и как объяснить те скачки на графике?
Когда вы создаете пустой массив, PHP создает его с определенным размером. Если вы заполняете массив и в какой-то момент достигаете и превышаете этот размер, то создается новый массив с вдвое большим размером, все элементы копируются в него и старый массив уничтожается.
Подробнее:
-
Статья на Хабр — Как устроены массивы в PHP
Как выделяется память под массив, передаваемый в ф-ю в качестве аргумента?
Массив передается в функцию изначально по ссылке (а не по значению, как принято думать); поэтому лишняя память не расходуется; но при любом его изменении создается уже копия этого массива, т.о. выделяется память под копию этого массива.
Это механизм copy-on-write («копирование при записи»), используемый в PHP для передачи массивов в функции.
-
При передаче массива в функцию передается указатель на исходный массив, а не копия. Память не расходуется.
-
Пока массив только читается — изменений нет и копия не создается.
-
Как только происходит запись в массив (изменение элемента) — создается полная копия массива и дальнейшие операции уже идут с этой копией.
-
Исходный массив, переданный в функцию, остается неизменным.
Таким образом оптимизируется производительность и память при передаче массивов в функции в PHP. Этот механизм автоматически применяется PHP при работе с массивами.
Что нового в последних версиях php?
Версия php |
Что нового |
Подробнее |
---|---|---|
8.0 |
PHP 8.0 был выпущен в ноябре 2020 года и включает ряд новых функций и улучшений. Некоторые из них включают: Добавлены новые синтаксические конструкции:
Улучшения в существующих функциях:
Улучшения производительности:
Другие изменения:
|
|
8.1 |
PHP 8.1 представляет несколько новых функций и улучшений, включая:
|
|
8.2 |
|
|
8.3 |
Подробнее :
-
статья в моем блоге: Что нового в php 8.1 ?
-
youtube: видео презентация отуса
-
wiki:
В чем разница между jit и opcache
Вот основные различия между jit и opcache в PHP:
-
JIT (Just-In-Time) компиляция — это технология, которая компилирует PHP код в машинный код во время выполнения скрипта. Это позволяет ускорить выполнение кода, так как машинный код выполняется быстрее, чем интерпретируемый bytecode. JIT был введен в PHP с версии 7.4.
-
OPcache — это кэш компилированного bytecode, который сохраняет предварительно скомпилированный bytecode скриптов PHP в памяти. Это позволяет избежать повторной компиляции скриптов при каждом запросе, что даёт прирост производительности. OPcache присутствует в PHP начиная с версии 5.5.
-
JIT работает во время выполнения кода и компилирует его в машинные инструкции «на лету», в то время как OPcache просто кэширует уже скомпилированный bytecode между запросами.
-
JIT может дополнительно оптимизировать выполнение кода за счёт информации о типах данных и других факторов во время выполнения. OPcache такой оптимизации не производит.
-
Таким образом, они решают похожие, но не идентичные задачи оптимизации. JIT и OPcache лучше использовать вместе для максимальной производительности PHP.
Что такое zend vm
Zend VM — это виртуальная машина (ВМ) для выполнения bytecode в PHP.
Основные функции Zend VM:
-
Интерпретация bytecode — Zend VM выполняет предварительно скомпилированный в bytecode код PHP, интерпретируя его.
-
Управление памятью — выделяет и освобождает память в процессе выполнения скриптов.
-
Обеспечение безопасности — проверяет доступ к ресурсам в соответствии с настройками безопасности.
-
Оптимизации — применяет различные оптимизации исполнения кода, например кэширование часто используемых функций.
-
Поддержка расширений — интегрирует расширения в ВМ, обеспечивая возможность вызова их функций.
Zend VM является ключевым компонентом PHP, обеспечивающим выполнение скриптов. Современные версии Zend VM используют JIT-компиляцию кода в машинные инструкции для ускорения выполнения.
Таким образом, Zend VM реализует среду выполнения PHP, обеспечивающую производительность, безопасность и поддержку различных возможностей языка.
В чем разница между генератором и итератором?
Основные различия между генераторами и итераторами в PHP:
-
Генераторы (yield) позволяют возвращать значения по одному при каждом вызове функции или метода. Итераторы реализуют интерфейс, который позволяет поочередно получать элементы из коллекции.
-
Генераторы более легковесны и производительны, т.к. возвращают значения «на лету» без создания коллекции. Итераторы предполагают наличие некой структуры данных, по которой происходит итерация.
-
С генераторами код получается более линейным и читаемым. Итераторы требуют реализации определенных методов интерфейса.
-
Итераторы можно перематывать, получая предыдущие значения. Генераторы движутся строго вперед.
-
Генератор может как возвращать значения через yield, так и получать входные данные через отправку значений в генератор. Итераторы только возвращают значения.
-
Итераторы хороши для обхода структур данных. Генераторы удобны для ленивых вычислений значений по запросу.
Таким образом, генераторы и итераторы решают схожие задачи, но генераторы обычно предпочтительнее для «ленивых» вычислений, а итераторы — для работы с данными. Их можно комбинировать в одном коде.
Как работает сборщик мусора в phpM
В PHP работает автоматический сборщик мусора на основе подсчета ссылок.
Основные принципы его работы:
-
Память выделяется для каждого объекта при создании.
-
Вместе с выделенной памятью сохраняется счетчик ссылок на объект.
-
При присваивании объекта переменной или передаче в функцию счетчик увеличивается.
-
При удалении переменной или возврата из функции счетчик уменьшается.
-
Когда счетчик достигает 0 — память объекта освобождается сборщиком мусора.
-
Сборка мусора запускается автоматически при освобождении памяти.
-
Также сборщик мусора может запускаться принудительно при помощи функции gc_collect_cycles().
-
Сборщик мусора ищет циклические ссылки между объектами и освобождает их.
Таким образом обеспечивается автоматическое управление памятью в PHP без необходимости ручного удаления объектов.
Что такое z_val ?
z_val
в PHP — это внутреннее представление значения переменной в Zend Engine.
Когда в скрипте PHP объявляется переменная, для нее выделяется место в памяти. Это место в памяти содержит не само значение переменной, а указатель (zval
) на это значение.
Структура zval содержит следующие поля:
-
zvalue
— указатель на фактическое значение переменной -
type
— тип значения (string, int, double, array и т.д.) -
is_ref
— является ли переменная ссылкой -
refcount
— счетчик ссылок для сборщика мусора
Таким образом, z_val
— это указатель на структуру zval, которая описывает переменную, ее тип и значение.
Использование zval позволяет реализовать в Zend Engine:
-
Проверку типов
-
Корректную работу ссылочных переменных
-
Подсчет ссылок для сборщика мусора
-
Изменение типа значения при необходимости
То есть z_val является базовой абстракцией, которая представляет значение переменной внутри PHP интерпретатора.
Как добиться единого code-style в команде разработчиков?
Кратко: линтеры, код-ревью, форматтеры кода
Вот несколько способов добиться единого code style в команде разработчиков:
-
Выбрать готовое php code style руководство, например PSR стандарты. Описать стандарты и следовать им.
-
Настроить линтер (phpcs, phpstan) с нужными правилами форматирования кода и запускать перед коммитом в репозиторий.
-
Использовать форматтеры кода (phpcbf) для автоматического форматирования перед коммитом.
-
Встроить проверку code style в CI/CD, чтобы код без соблюдения стандартов не попадал в основную ветку.
-
Использовать Prettier или другие плагины форматирования в редакторах кода разработчиков.
-
Проводить code review с проверкой соответствия стандартам.
-
Организовать обучение команды по выбранным стандартам PSR
Главное — это выбрать конкретные стандарты, интегрировать их в процесс разработки и следить за соблюдением. Со временем это войдет в привычку разработчиков.
Как отловить ошибки в коде , которые не видит линтер?
В дополнение к линтерам, для поиска ошибок в коде, которые они не выявляют, можно использовать следующие способы:
-
Unit тесты — позволяют выявить логические ошибки в коде.
-
Интеграционное тестирование — выявляет ошибки взаимодействия между компонентами системы.
-
Функциональное тестирование — имитирует реальные сценарии и выявляет ошибки в бизнес-логике.
-
Тестирование производительности и нагрузки — помогает найти узкие места и ошибки, связанные с производительностью.
-
Ручное тестирование — проверка критических сценариев вручную.
-
Анализ покрытия кода тестами — показывает, какие части кода не покрыты тестами.
-
Статический анализ кода — выявляет сложные участки кода, подверженные ошибкам.
-
Трассировка и логирование — отладка и логи помогают выявить ошибки в работающем коде.
-
Code review — проверка кода опытными разработчиками.
Комбинация этих методов позволяет повысить качество кода и снизить количество ошибок.
Как обеспечить тестирование в БД ?
Вот несколько способов для тестирования кода, работающего с базой данных:
-
Использовать отдельную тестовую БД, отличную от продакшен. Перед тестами накатывать тестовые данные с помощью миграций или фикстур.
-
Транзакции — каждый тест выполнять в отдельной транзакции, которая откатывается по завершении теста. Это изолирует данные тестов друг от друга.
-
Использовать моки для эмуляции работы БД в юнит-тестах без обращения к реальной БД. Например, с помощью PHPUnit можно мокнуть PDO.
-
Фабрики данных — генерировать тестовые данные через factory методы, а не через фикстуры. Это даёт большую гибкость.
-
Snapshot testing — сохранять снапшоты данных в БД после операций и сравнивать с ожидаемым результатом.
-
Использовать библиотеки вроде DBUnit для автоматизации тестирования с базой данных.
-
Настроить очистку тестовых данных после прохождения тестов, чтобы сохранять БД в чистом состоянии.
Главное — изолировать тесты друг от друга и использовать тестовые данные, а не продакшен. Это позволит качественно протестировать работу с БД.
В phpunit есть моки?
Да, в PHPUnit есть возможность создавать мок-объекты (mock objects) для тестирования.
Основные способы создания моков в PHPUnit:
-
Моки для интерфейсов — используя метод
createMock()
можно создать мок-объект для заданного интерфейса или класса. -
Заглушки методов — метод
willReturn()
позволяет задать возвращаемое моком значение для указанного метода. -
Ожидание вызовов — с помощью
expect()
иmethod()
можно задать ожидаемые вызовы методов мока. -
Верификация — метод
verify()
проверяет, что ожидаемые вызовы произведены. -
Задание аргументов —
when() , with()
позволяют указать аргументы ожидаемых вызовов. -
Фиксация моков — функция
getMock()
сохраняет созданный мок в реестре моков PHPUnit.
Также в PHPUnit есть специальные мок-объекты для работы с временем, пользовательским вводом, отправкой HTTP запросов и другими случаями.
Моки позволяют эмулировать объекты и их поведение при тестировании для изоляции классов и методов. Это очень полезный инструментарий при юнит-тестировании.
Задачи
№1 : Добавление кэширования
Добавить кэширование в метод
getByIds
(интерфейс для кеша указан), т.о. чтобы ИД кэшировались и обращение к API шло только если данных нет в кеше.
<?php /** * Интерфейс к сервису кэша (имплементировать класс не нужно) */ interface Cache { /** * @param int[] $ids * * @return array<int,Item|null> */ public function get(array $ids): array; /** * @param array<int,Item> $items */ public function set(array $items): void; } /** * Интерфейс к http-клиенту сервиса объявлений (имплементировать класс не нужно) */ interface Client { /** * @param int[] $ids * * @return array<int,Item> */ public function get(array $ids): array; } /** * Класс служит обёрткой над http-клиентом к сервису, хранящему информацию об объявлениях. * Метод `getByIds`, принимает массив идентификаторов и возвращающий массив найденных сущностей Item. * Нужно дополнить метод `getByIds`, чтобы он мог использовать кэш. */ class ItemService { public function __construct(private Client $client) { } /** * @param int[] $ids * * @return array<int,Item> */ public function getByIds(array $ids): array { // @todo Написать логику работы с кешом return $this->client->get($ids); } }
Мое решение №1 (не оптимальное):
class ItemService { private Client; private Cache; public function __construct(Client $client, Cache $cache) { $this->client = $client; $this->cache = $cache; } /** * @param int[] $ids * * @return array<int,Item> */ public function getByIds(array $ids): array { $cahceIds = $this->cache->get($ids); $needIds = []; foreach($cacheIds as $cid => $data) { if($data === null) { $res[$cid] = $this->client->get([$cid]); } else { $res[$cid] = $data; } } return $res; } }
Не оптимально тем, что будет сделано N обращение к API за недостающими данными (а должно быть одно)
Решение №2: Более верное
/** * @param int[] $ids * @return array<int,Item> */ public function getByIds(array $ids): array { $caсheIds = $this->cache->get($ids); $needIds = []; foreach($cacheIds as $cid => $data) { if($data === null) { $needIds[] = $cid; } else { $res[$cid] = $data; } } if(count($needIds)) { $needData = $this->client->get($needIds); foreach($needData as $key => $data) { $res[$key] = $data; } $this->cache->set($needData); } return $res; }
Финальное решение тут
Задача №2: чтение большого лога
В коде ниже написать тело метода
numberOfErrors
т.о., чтобы в нем считалось кол-во ошибок указанного типа в лог-файле. Фишка в том, что файл м.б. большой (много Гб), и скрипт не должен падать из-за лимитов по памяти.
<?php declare(strict_types=1); // Некий класс ответа на запрос из нашего фреймворка. Приведен просто для референса class Response { public function __construct( public string $content, public int $code ) { } // ... } class LogController { /** * Метод numberOfErrors возвращает в ответ json, со следующей структурой: * { * "found_errors": <int> * } * * Он выполняется максимум X мс, которые мы можем задать в конфиге. * Он проходится по файлу log.txt и ищет там ошибки с кодом $errorCode. * Он возвращает количество найденных ошибок за период времени. * * На сервере для PHP процесса выделяется 250 mb памяти. Размер файла log.txt - 10gb * Файл расположен в корне (/log.txt), его содержимое: * <timestamp>;<error_code> */ #[Route('numberOfErrors/{errorCode}')] public function numberOfErrors(int $errorCode): Response { // написать имплементацию метода return new Response('', 200); } }
Мое решение №1: с помощью генераторов
public function numberOfErrors(int $errorCode): Response { $cnt = 0; // написать имплементацию метода foreach($this->readLine("log.txt") as $line) { $exp = explode(";", $line); $errCode = $exp[1] ?? null; if($errCode == $errorCode) { $cnt++; } } $json = jsone_encode(['found_errors' => $cnt]) return new Response($json, 200); } private function readLine(string $fname) { if(!file_exists($fname)) { return null; } if($this->f === null) { $this->f = fopen($fname, "r"); } while(!feof($this->f)) { $str = fgets($f); // read yield $str; } fclose($this->f); } }
Решение №2: (поправленное после обсуждений)
Генератор тут не нужен, это over-engineering
public function numberOfErrors(int $errorCode): Response { $cnt = 0; $fname = self::FNAME; if(!file_exists($fname)) { $json = json_encode(['error' => 'file not found']); return new Response($json, 500); } if($this->f === null) { $this->f = fopen($fname, "r"); } // написать имплементацию метода while(!feof($this->f)) { $line = fgets($this->f); // read new line $exp = explode(";", $line); $errCode = $exp[1] ?? null; if($errCode == $errorCode) { $cnt++; } } fclose($this->f); $json = json_encode(['found_errors' => $cnt]); return new Response($json, 200); }
Доп. вопрос: как выдать результат спустя 100 микро-сек запуска ?
То есть если сервером задано ограничение на время запуска скрипта в ~100 мсек, и нельзя его превысить, и нужно выдать результат (кол-во посчитанных ошибок) — сколько успели посчитать за это время.
Мое первое предположение : использовать файберы? — “нет, слишком сложно” (хотя это то что надо, имхо)
Решение: можем считать время работы скрипта и выходить из цикла чтения спустя Х мсек (верно):
$ts = (microtime(true)); // написать имплементацию метода while(!feof($this->f)) { $line = fgets($this->f); // read new line $exp = explode(";", $line); $errCode = $exp[1] ?? null; if($errCode == $errorCode) { $cnt++; } // script execution time limit $time = (microtime(true) - $ts); if($time >= self::TIMEOUT) { break; } }
Тут еще константу желательно вынести в конфиг, о чем также меня спросили (т.к. хардкод значений не приветствуется нигде):
if($time >= app::getConfig()->getVar('exec_timeout')) { ...
Финальное решение тут
Интервью далее в Авито
-
HR -
Техн. скриннинг -
Live-кодинг -
Платформа (была сейчас) -
Алгоритмы — на моем языке
-
Архитектура / System design — на senior
-
Софт скиллы: знакомство с командой, product-owner
Рекомендую для прокачки
Скачайте мою книгу по подготовке к собеседованиям веб-разработчика. Здесь подробно разобрано множество вопросов и задач с интервью, накопленные мной за 10+ лет опыта.
Нет Ответов