Skip to content

Backend Interview Questions 2023 - это обширный ресурс помогающий в подготовке к техническим собеседованиям на позицию Backend Developer

Notifications You must be signed in to change notification settings

evasilev/backend-interview-questions

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 

Repository files navigation

Backend Interview Questions 2023

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

Вопросы условно делятся на 2 категории:

1️⃣ Общие вопросы
2️⃣ Вопросы по языкам программирования.

Инструкции по применению

На данный момент репозиторий содержит:

➡️ Теоретические вопросы: 216 -> 225
➡️ Практические задачи: 27

1️⃣ Настоятельно рекомендую, прежде чем открывать ответ на вопрос попробовать ответить на него самостоятельно.
2️⃣ Если ответить не получается, попробуйте выдумать ответ основываясь на уже имеющихся у вас знаниях.
3️⃣ И только если вы в полном ступоре открывайте ответ на вопрос.

▶️ У практических задач рядом с вопросом будет стоять сложность: Static Badge Static Badge Static Badge


P.S.

*️⃣ Вы будете благодарны себе если выполните инструкцию в точности как написано.

P.S.S.

⏺️ Если вы заметили неточность.
⏺️ У вас есть свой вопрос который вы добавили бы в список.
⏺️ Вы недавно проходили собеседование.
🆓 То смело пишите мне в Telegram я обязательно исправлю/добавлю вопрос/ответ.


Features

Данный репозиторий будет обновляться по мере появления новых вопросов и актуальной информации.
Также в разделах будут добавлены другие языки программирования (с этим я прошу помощи у неравнодушного сообщества GitHub)
Давайте поможем друг другу и новичкам пришедшим в мир IT 🙌


logo


Вопросы и ответы

1️⃣ Общие темы:

Общие вопросы
  • Вопрос №1: [ Что такое микросервисы? ]

    Ответ
    • Микросервисы — это подход к разработке программного обеспечения, при котором большое приложение разбивается на меньшие, автономные компоненты. Каждый микросервис представляет собой отдельный модуль, который реализует определенный функционал и может работать независимо от других модулей. Эти модули обычно взаимодействуют друг с другом через API или событийно-ориентированную архитектуру.

  • Вопрос №2: [ Какие преимущества у микросервисной архитектуры по сравнению с монолитом? А какие недостатки? ]

    Ответ
    • Преимущества:

      • Гибкость: Можно использовать разные технологии и языки программирования для разных микросервисов.
      • Масштабируемость: Легче масштабировать отдельные компоненты.
      • Распределение работы: Разные команды могут работать над разными сервисами параллельно.
      • Быстрый цикл разработки: Изменения в одном микросервисе могут быть развернуты независимо от других.
    • Недостатки:

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

  • Вопрос №3: [ Как быть с консистентностью данных между несколькими микросервисами? ]

    Ответ
    • Консистентность данных в микросервисной архитектуре — сложная задача. Один из подходов — использование распределенных транзакций, но это может привести к проблемам производительности и доступности. Другой подход — "eventual consistency", где система стремится обеспечить консистентность данных в течение некоторого времени. Для этого часто используют шины сообщений и системы очередей, такие как Kafka или RabbitMQ, чтобы синхронизировать данные между сервисами.

  • Вопрос №4: [ Что такое сине-зеленый деплой (Blue-Green Deployment)? ]

    Ответ
    • Сине-зеленый деплой — это метод развертывания приложений, при котором создается полностью независимое окружение (зеленое), идентичное текущему продуктивному(синему). После проверки новой версии приложения в зеленом окружении, трафик переключается на это окружение, сделав его новым продуктивным. Этот метод позволяет мгновенно откатываться к предыдущей версии, если что-то пошло не так, так как синее окружение остается нетронутым.

    • Преимущества:

      • Быстрый откат: Если в новой версии есть проблемы, можно быстро вернуться к старой версии.
      • Нулевое время простоя: Переключение трафика происходит мгновенно, что исключает простои.

  • Вопрос №5: [ Что такое рефлексия? ]

    Ответ
    • Рефлексия в программировании — это механизм, который позволяет программам исследовать информацию о типах и структурах данных во время выполнения. В Go рефлексия основана на двух ключевых типах: Type и Value, которые определены в пакете reflect.

      С помощью рефлексии можно:

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

      Зачем это нужно? Рефлексия часто используется в ситуациях, где типы данных неизвестны до времени выполнения. Например, она полезна при работе с библиотеками для маршалинга и анмаршалинга данных (например, JSON, XML), создании ORM, фреймворков для тестирования и многом другом.

      Осторожно!!! Несмотря на свою мощь, рефлексию следует использовать осторожно:

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

      Таким образом, рефлексия — мощный, но "острый" инструмент, и его следует использовать разумно.


  • Вопрос №6: [ Что такое асинхронность? ]

    Ответ
    • Вычисления в системе могут идти двумя способами:
      • синхронно - это когда код выполняется последовательно;
      • асинхронно - это когда операцию мы можем выполнять не дожидаясь результата на месте. Обычно подразумевается, что операция может быть выполнена кем-то на стороне.

  • Вопрос №7: [ Что такое параллельность? ]

    Ответ
    • Вычисления будут являться параллельным только в том случае, если они выполняются одновременно. Как пример можно привести процесс ремонта в доме. У нас есть несколько мастеров-универсалов, каждый из которых выполняет работы на своем объекте под ключ. При этом производительность мастеров не зависит друг от друга, так как их работа не пересекается.

  • Вопрос №8: [ Что такое конкурентность? ]

    Ответ
    • Конкурентность обеспечивает выполнение нескольких задач посредством переключения контекста. Конкурентные вычисления реализуются на одном ядре системы. Как пример приведем тот же процесс ремонта, но с другими вводными условиями. Теперь мы имеем один объект, на который привлекаем специалистов разного профиля: по демонтажным работам, электрике, подготовке стен и полов, отделке. При этом у нас часто возникают ситуации, когда хозяин уже в процессе подготовки стен, решает, что вот эта стена ему все же не нужна, и на сцену опять выходят демонтажники. Такой процесс организации работ можно назвать конкурентным, так как наши мастера уступают место друг другу, одновременно клеить обои и ломать стены они не могут.

  • Вопрос №9: [ Что такое замыкание? ]

    Ответ
    • Замыкание (closure) в программировании — это функция, которая имеет доступ к переменным из своего лексического контекста. То есть переменным, которые были доступны в момент создания замыкания. Замыкания "запоминают" окружение, в котором они были созданы, и могут использовать переменные из этого окружения даже после того, как контекст выполнения уже исчез.

  • Вопрос №10: [ Что такое gRPC? ]

    Ответ
    • gRPC (gRPC Remote Procedure Calls) — это современный, открытый фреймворк для удаленного вызова процедур (RPC), разработанный в Google. Он использует протокол HTTP/2 для транспорта и протокол Protocol Buffers (обычно сокращенно Protobuf) для сериализации структурированных данных. gRPC предлагает множество особенностей, таких как двусторонняя потоковая передача данных, мультиплексирование, аутентификация на основе SSL/TLS и другие.

    • Основные особенности gRPC:

      • Производительность: Благодаря HTTP/2 и Protocol Buffers gRPC обычно более производителен по сравнению с другими механизмами RPC или REST.
      • Поддержка множества языков: gRPC имеет официальные библиотеки для большинства популярных языков программирования, включая Go, Java, C#, Node.js, Python и многие другие.
      • Потоковая передача данных: gRPC поддерживает однонаправленную и двунаправленную потоковую передачу данных, что делает его очень гибким для построения сложных распределенных систем.
      • Жестко типизированный протокол: Использование Protocol Buffers обеспечивает жесткую типизацию, что улучшает проверку данных и упрощает чтение кода.
      • Плагинная архитектура: gRPC может быть расширен для поддержки различных методов аутентификации, балансировки нагрузки, повторных попыток и других сложных сценариев работы в сети.
      • Богатый набор инструментов: Благодаря своей популярности и активному развитию, существует множество инструментов для мониторинга, трассировки и отладки gRPC-приложений.
    • gRPC часто используется в микросервисных архитектурах и в других распределенных системах, где требуется высокая производительность, надежная типизация и сложные сценарии взаимодействия между компонентами.


  • Вопрос №11: [ Как бы ты классифицировал языки программирования? ]

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

      • Процедурные языки (C, Pascal) — ориентированы на процедуры и функции.
      • Объектно-ориентированные языки (Java, C++, Python) — ориентированы на объекты и классы.
      • Языки с компонентной архитектурой (C#, .NET) — ориентированы на модульность и повторное использование кода.
    • Декларативные языки
      В этих языках программисты указывают, что именно они хотят сделать, но не как. Сюда входят:

      • Функциональные языки (Haskell, Lisp, ML) — фокус на математических функциях и иммутабельных данных.
      • Логические языки (Prolog) — фокус на логических утверждениях и выводах.
      • Языки запросов (SQL) — специализированы на описании запросов к базам данных.
      • Языки разметки и стилей (HTML, XML, CSS) — описание структуры и представления данных.
    • Многопарадигмальные языки
      Некоторые языки, такие как Python, Scala или JavaScript, можно отнести к многопарадигмальным, так как они поддерживают несколько стилей программирования. Например, в Python можно писать и в императивном, и в объектно-ориентированном, и даже в функциональном стилях.

    • Другие классификации
      Кроме стилей программирования, языки также классифицируют по уровню абстракции (низкоуровневые vs высокоуровневые), области применения (системные, встроенные, научные, веб-разработка и т.д.), типизации (статическая vs динамическая) и другим критериям.


Сеть и всё что с ней связано
  • Вопрос №1: [ В чем отличие протоколов TCP и UDP? ]

    Ответ
    • TCP (Transmission Control Protocol)

      • Ориентирован на установление надежного соединения.
      • Ошибки корректируются; потерянные или поврежденные пакеты пересылаются.
      • Поддерживает управление потоком и перегрузкой.
      • Нормально работает в условиях высокой задержки.
    • UDP (User Datagram Protocol)

      • Безусловный протокол, не устанавливает соединение.
      • Ошибки не корректируются; потерянные пакеты не восстанавливаются.
      • Не поддерживает управление потоком и перегрузкой.
      • Обычно быстрее, чем TCP.
    • Когда UDP предпочтительнее:

      • Потоковое медиа, онлайн-игры, VoIP — там, где задержка критична и потеря пакетов допустима.

  • Вопрос №2: [ Какие еще протоколы существуют? ]

    Ответ
    • Транспортный уровень (как TCP и UDP):

      • SCTP (Stream Control Transmission Protocol) — протокол, предназначенный для передачи данных с поддержкой множественных потоков и устойчивый к ошибкам.
      • CCP (Datagram Congestion Control Protocol) — протокол, предназначенный для передачи потоковых медиа.
    • Сетевой уровень:

      • IP (Internet Protocol) — протокол маршрутизации.
      • ICMP (Internet Control Message Protocol) — протокол управляющих сообщений.
      • OSPF (Open Shortest Path First) — протокол динамической маршрутизации.
    • Канальный уровень:

      • Ethernet — наиболее распространенный протокол канального уровня.
      • Wi-Fi — набор стандартов для беспроводных локальных сетей.
    • Прикладной уровень:

      • HTTP/HTTPS (HyperText Transfer Protocol/Secure) — протокол передачи гипертекста.
      • FTP (File Transfer Protocol) — протокол передачи файлов.
      • SMTP (Simple Mail Transfer Protocol) — протокол для передачи электронной почты.
      • DNS (Domain Name System) — система преобразования доменных имен в IP-адреса.
      • MQTT (Message Queuing Telemetry Transport) — протокол мессенджинга для IoT устройств.
      • Это далеко не исчерпывающий список, и существует множество других протоколов для различных специфических задач и сценариев использования.

  • Вопрос №3: [ Что такое http и в чем отличие от https? ]

    Ответ
    • HTTP (HyperText Transfer Protocol) и HTTPS (HyperText Transfer Protocol Secure) — это протоколы, используемые для передачи данных между веб-браузером и веб-сервером. Оба протокола используются в Интернете для загрузки и отправки веб-страниц, файлов, изображений и других ресурсов.

    • Основные различия между HTTP и HTTPS:

      • Безопасность: Самое основное различие — уровень безопасности. HTTPS использует SSL/TLS протоколы для шифрования данных, что делает его надежнее для передачи конфиденциальной информации, такой как пароли, номера кредитных карт и т.д.
      • Порт: По умолчанию HTTP использует порт 80, а HTTPS — порт 443.
      • Скорость: Из-за дополнительного слоя шифрования HTTPS может быть немного медленнее, чем HTTP. Однако современные оптимизации и широкая поддержка HTTP/2 сильно уменьшают этот разрыв.
      • SEO: Поисковые системы, такие как Google, предпочитают сайты, использующие HTTPS, и могут выставлять их выше в результатах поиска.
      • Индикатор безопасности: В адресной строке браузера сайты, использующие HTTPS, обычно отображаются с зеленым замком или другим индикатором безопасности.
      • Сертификаты: Для работы с HTTPS требуется SSL-сертификат, который подтверждает, что данный веб-сервер действительно принадлежит указанному домену.
      • Данные в URL: В HTTPS параметры в URL также шифруются, в отличие от HTTP, где они могут быть прочитаны.
      • Промежуточные узлы: HTTPS затрудняет просмотр или модификацию передаваемых данных третьими сторонами (например, ман-в-середине атаки), поскольку данные шифруются.
    • Таким образом, основное различие между HTTP и HTTPS заключается в уровне безопасности. HTTPS рекомендуется для всех сайтов, особенно для тех, которые собирают и хранят конфиденциальную информацию.


  • Вопрос №4: [ Что произойдет если я введу некий адрес в строку поиска и нажму Enter? ]

    Ответ
    • Введение адреса в строку поиска веб-браузера и нажатие "Enter" инициирует сложный процесс, в котором участвуют различные компоненты, включая ваш компьютер, сетевые протоколы и удаленный веб-сервер. Вот как это обычно происходит:

      • Анализ URL: Браузер анализирует введенный URL, чтобы определить, является ли это поисковым запросом или конкретным веб-адресом.
      • Кэширование и Локальные Ресурсы: Браузер проверяет локальный кэш и другие ресурсы (например, файлы hosts), чтобы увидеть, сохранена ли уже нужная страница или известен ли IP-адрес для данного доменного имени.
      • DNS-запрос: Если информация не найдена локально, браузер отправляет DNS-запрос на серверы доменных имен для разрешения доменного имени на IP-адрес.
      • Установление Соединения: После получения IP-адреса браузер устанавливает соединение с веб-сервером, используя протокол HTTP или HTTPS.
      • HTTP-Запрос: Браузер отправляет HTTP-запрос на веб-сервер. Запрос обычно включает в себя метод (обычно "GET" для извлечения данных), заголовки и, возможно, тело запроса.
      • Обработка Запроса: Веб-сервер обрабатывает запрос, что может включать в себя различные задачи, такие как выполнение кода на стороне сервера, запросы к базам данных и т.д.
      • HTTP-Ответ: Веб-сервер отправляет ответ обратно в браузер. Ответ включает в себя HTTP-статус (например, 200 для успешного запроса, 404 для "Не найдено"), заголовки ответа и тело ответа, которое обычно содержит запрашиваемую веб-страницу.
      • Рендеринг Страницы: Браузер анализирует HTML, CSS и JavaScript, содержащиеся в теле ответа, и отображает страницу на экране.
      • Загрузка Ресурсов: Веб-страницы часто содержат дополнительные ресурсы, такие как изображения, стили и скрипты, которые также нужно загрузить. Браузер делает дополнительные HTTP-запросы для этих ресурсов.
      • Завершение: Страница полностью загружена и отображена в браузере, и пользователь может взаимодействовать с ней.
    • Это очень упрощенное описание процесса, и на каждом этапе может происходить много дополнительных действий, включая обработку кук, кэширование, редиректы и другие.


  • Вопрос №5: [ Что такое MAC-адрес? Для чего его используют? ]

    Ответ
    • MAC-адрес — это уникальный идентификационный номер или код, используемый для идентификации отдельных устройств в сети. Пакеты, отправляемые по Ethernet, всегда поступают с MAC-адреса и отправляются на MAC-адрес. Если сетевой адаптер получает пакет, он сравнивает MAC-адрес назначения пакета с собственным MAC-адресом адаптера.

  • Вопрос №6: [ Что такое IP-адрес? ]

    Ответ
    • Адрес Интернет-протокола (IP-адрес) — это числовая метка, присвоенная каждому устройству, подключенному к компьютерной сети, которая использует Интернет-протокол для связи. IP-адрес выполняет две основные функции: идентификацию хоста или сетевого интерфейса и адресацию местоположения.

  • Вопрос №7: [ Что такое маска подсети? ]

    Ответ
    • Это числовой параметр, используемый в сетях для определения диапазона IP-адресов, которые принадлежат одной и той же подсети. Маска подсети определяет, какая часть IP-адреса относится к сети, а какая - к устройствам в этой сети. Маска подсети представляет собой последовательность битов (обычно в виде четырех чисел, разделенных точками, например, 255.255.255.0), где каждый бит может быть установлен в 1 или 0. Биты, установленные в 1, обозначают часть IP-адреса, относящуюся к сети, а биты, установленные в 0, обозначают часть адреса, которая отведена для устройств в этой подсети. Например, если IP-адрес имеет вид 192.168.1.100, а маска подсети 255.255.255.0, то первые 24 бита этого IP-адреса (так как установлено 24 бита в маске подсети) используются для обозначения сети, а оставшиеся 8 бит отводятся для устройств в этой подсети. Это позволяет разделять сеть на подсети и управлять IP-адресами и маршрутизацией внутри сети. Маска подсети важна для правильной настройки сетевых устройств, чтобы они могли определить, какие адреса находятся в той же подсети, и какие должны быть переданы через маршрутизатор для доставки к удаленным сетям.

  • Вопрос №8: [ Что такое частный IP-адрес? В каких сценариях/проектах систем его следует использовать? ]

    Ответ
    • Частный IP-адрес (Private IP Address) - это IP-адрес, который назначается устройству или хосту внутри частной сети, обычно внутри организации, и не доступен напрямую из Интернета. Частные IP-адреса предназначены для использования в локальных сетях, и их использование помогает разделить адресное пространство Интернета между различными организациями, предотвращая конфликты IP-адресов и обеспечивая безопасность и конфиденциальность локальной сети.

      Существует несколько стандартных диапазонов частных IP-адресов, определенных RFC 1918:
      10.0.0.0 - 10.255.255.255 (10.0.0.0/8): Этот диапазон содержит 16 777 216 адресов и широко используется в больших организациях.
      172.16.0.0 - 172.31.255.255 (172.16.0.0/12): Этот диапазон содержит 1 048 576 адресов и часто используется в средних по размеру сетях.
      192.168.0.0 - 192.168.255.255 (192.168.0.0/16): Этот диапазон содержит 65 536 адресов и является одним из самых распространенных для домашних сетей и малых офисов.

    • Частные IP-адреса часто используются в следующих сценариях и проектах:

      • Внутренние корпоративные сети: В офисных сетях и сетях предприятий используются частные IP-адреса для всех устройств внутри организации. Это позволяет легко управлять и масштабировать сеть.
      • Домашние сети: Для домашних сетей, подключенных к Интернету через маршрутизатор, частные IP-адреса используются для устройств в домашней сети, обеспечивая их безопасность и изоляцию от Интернета.
      • Виртуальные частные сети (VPN): В VPN-сетях используются частные IP-адреса для обеспечения безопасного и зашифрованного соединения между удаленными устройствами и корпоративной сетью.
      • Тестирование и разработка: В среде разработки и тестирования частные IP-адреса могут использоваться для изоляции тестовых сред от боевых сетей.
      • Локальные игровые сети: В локальных сетях для игр на консолях и ПК часто используются частные IP-адреса для подключения устройств друг к другу в играх.
      • Частные IP-адреса не могут использоваться напрямую в Интернете, поэтому для устройств, которым требуется общедоступная сетевая связь, используются механизмы Network Address Translation (NAT) или прокси-серверы для перевода частных адресов в публичные.

  • Вопрос №9: [ Какова функция протокола DHCP в сети? ]

    Ответ
    • DHCP (Dynamic Host Configuration Protocol) используется для автоматического назначения IP-адресов и других сетевых параметров устройствам в сети.

  • Вопрос №10: [ Объясните вкратце, как работает DNS ]

    Ответ
    • DNS (Domain Name System) – это система, которая переводит доменные имена (например, www.example.com) в IP-адреса, чтобы устройства могли общаться в сети.

  • Вопрос №11: [ Какова роль маршрутизатора в сети? В чем разница между маршрутизатором и коммутатором? ]

    Ответ
    • Маршрутизатор направляет трафик между разными сетями. Коммутатор оперирует в рамках одной локальной сети и соединяет устройства внутри этой сети.

Операционная система
  • Вопрос №1: [ Можно ли убить поток внутри определенного процесса командой kill? ]

    Ответ
    • Обычно команда kill убивает процессы, а не отдельные потоки. В Linux потоки являются частью процесса и не могут быть убиты независимо от него командой kill.

  • Вопрос №2: [ Что такое операционная система? ]

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

  • Вопрос №3: [ Что такое процесс? ]

    Ответ
    • Процесс — это выполняющаяся программа в управлении операционной системы. Процесс имеет свой собственный участок памяти, состояние выполнения и системные ресурсы.

  • Вопрос №4: [ Что такое многозадачность? ]

    Ответ
    • Многозадачность — это способность операционной системы выполнять несколько задач (процессов) одновременно.

  • Вопрос №5: [ Что такое ядро ОС? ]

    Ответ
    • Ядро ОС — это основной компонент операционной системы, который управляет аппаратными и программными ресурсами компьютера.

  • Вопрос №6: [ Что такое драйвер? ]

    Ответ
    • Драйвер — это специальная программа, позволяющая операционной системе взаимодействовать с конкретным аппаратным обеспечением.

  • Вопрос №7: [ Что такое виртуальная память? ]

    Ответ
    • Виртуальная память — это технология, которая позволяет использовать часть жесткого диска как дополнение к оперативной памяти.

  • Вопрос №8: [ Что такое IPC (Inter-Process Communication)? ]

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

  • Вопрос №9: [ Что такое системный вызов? ]

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

  • Вопрос №10: [ что такое CPU-bound и IO-bound задачи? ]

    Ответ
    • Задачи, выполняемые в программе или системе, часто классифицируются как CPU-bound или IO-bound, в зависимости от того, какие ресурсы являются основными узкими местами для их выполнения.
    • CPU-bound задачи: Это задачи, для которых основное ограничение — это скорость процессора (CPU). Они требуют много вычислительных ресурсов и часто занимают процессор интенсивными вычислениями. Такие задачи будут быстрее выполняться на более быстром процессоре или при использовании оптимизированных алгоритмов. Примеры включают в себя сложные математические вычисления, рендеринг графики, обработку данных и так далее.
    • IO-bound задачи: Это задачи, для которых основное ограничение — это скорость ввода/вывода (I/O), такая, как операции чтения/записи на диск, сетевые операции или обращение к базе данных. Улучшение производительности таких задач обычно достигается путем оптимизации системы I/O, уменьшения количества I/O операций или асинхронного выполнения I/O, чтобы другие части программы могли продолжать работу, пока I/O операция завершается. Примеры включают в себя загрузку файлов, запросы к базе данных или сетевые запросы. В реальном мире многие задачи являются комбинацией CPU-bound и IO-bound операций. Определение типа вашей задачи может помочь вам определить, какие оптимизации могут дать наилучший результат. Например, добавление дополнительных ядер процессора может помочь с CPU-bound задачами, в то время как асинхронное программирование или использование быстрого SSD может помочь с IO-bound задачами.

Базы данных
  • Вопрос №1: [ Какая разница между реляционными vs не реляционными СУБД? ]

    Ответ

    SQL:

    • Плюсы:
      • Строгая схема: Помогает в поддержании целостности данных.
      • ACID-свойства: Поддержка транзакций с гарантированной Атомарностью, Согласованностью, Изолированностью и Долговечностью.
      • SQL: Богатый язык запросов, хорошо подходящий для сложных запросов.
      • Широкая поддержка: Огромное сообщество, много документации и инструментов.
      • Зрелость: Проверенные временем, надежные решения.
    • Минусы:
      • Горизонтальное масштабирование: Обычно сложнее масштабировать горизонтально по сравнению с NoSQL.
      • Сложность: SQL и реляционные схемы могут быть сложными для новичков.
      • Стоимость: Коммерческие решения могут быть дорогими.

    NoSQL:

    • Плюсы:
      • Масштабируемость: Обычно проще масштабировать горизонтально.
      • Гибкость схемы: Можно легко добавлять поля в данные.
      • Высокая производительность: Оптимизированы для больших данных и реального времени.
      • Разнообразие моделей данных: ключ-значение, документ-ориентированные, колоночные и графовые базы данных.
    • Минусы:
      • Недостаток стандартизации: Множество разных систем с разными API.
      • Сложность: Распределенные системы приносят собой сложности в управлении и обслуживании.
      • Недостаточная поддержка транзакций: Не все NoSQL-системы поддерживают ACID-транзакции.

    Когда выбрать NoSQL?

    • При необходимости горизонтального масштабирования.
    • Когда схема данных непостоянна или развивается со временем.
    • Для больших данных и обработки в реальном времени.

    Какие NoSQL решения знаешь?

    • MongoDB, Cassandra, Redis, и Couchbase.

    Трудности при работе с NoSQL:

    • Сложность управления распределенной системой.
    • Отсутствие стандартизированного языка запросов, как SQL.
    • Вопросы консистентности данных, особенно в распределенных системах.

  • Вопрос №2: [ Что такое индексы? Зачем они нужны? ]

    Ответ
    • Индексы в базах данных — это структуры, которые ускоряют операции выборки данных.
    • Они нужны для ускорения доступа к данным в таблице и эффективной работы операций выборки, сортировки и объединения.

  • Вопрос №3: [ Почему нельзя создать индексы на все поля в таблице? ]

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

  • Вопрос №4: [ Почему индекс не поможет в случае с полем пол (всего два значения)? ]

    Ответ
    • Индексы менее эффективны для полей с низкой кардинальностью (мало уникальных значений), потому что они не сильно улучшают скорость поиска.

  • Вопрос №5: [ Что такое транзакции? Для чего нужны? ]

    Ответ
    • Транзакции — это последовательности операций с базой данных, которые представляют собой единое целое. Нужны для обеспечения целостности данных и корректного выполнения нескольких операций.

  • Вопрос №6: [ Расскажи про уровни изоляции, ACID ]

    Ответ
    • Уровень изоляции READ UNCOMMITTED
      Уровень изоляции READ UNCOMMITTED предоставляет самую простую форму изоляции между транзакциями, поскольку он вообще не изолирует операции чтения других транзакций. Когда транзакция выбирает строку при этом уровне изоляции, она не задает никаких блокировок и не признает никаких существующих блокировок. Считываемые такой транзакцией данные могут быть несогласованными. В таком случае транзакция читает данные, которые были обновлены какой-либо другой активной транзакцией. А если для этой другой транзакции позже выполняется откат, то значит, что первая транзакция прочитала данные, которые никогда по-настоящему не существовали.
      Из четырех проблем одновременного конкурентного доступа к данным, описанных в предшествующем разделе, уровень изоляции READ UNCOMMITTED допускает три: грязное чтение, неповторяемое чтение и фантомы.
      Применение уровня изоляции READ UNCOMMITTED обычно крайне нежелательно и его следует применять только в тех случаях, когда точность данных не представляет важности или когда данные редко подвергаются изменениям.

    • Уровень изоляции READ COMMITTED
      Как уже упоминалось, уровень READ COMMITTED имеет две формы. Первая форма применяется в пессимистической модели одновременного конкурентного доступа, а вторая - в оптимистической. В этом разделе рассматривается первая форма этого уровня изоляции.
      Транзакция, которая читает строку и использует уровень изоляции READ COMMITTED, выполнят проверку только на наличие монопольной блокировки для данной строки. Если такая блокировка отсутствует, транзакция извлекает строку. (Это выполняется с использованием разделяемой блокировки.) Таким образом предотвращается чтение транзакцией данных, которые не были подтверждены и которые могут быть позже отменены. После того, как данные были прочитаны, их можно изменять другими транзакциями.
      Применяемые этим уровнем изоляции разделяемые блокировки отменяются сразу же после обработки данных. (Обычно все блокировки отменяются в конце транзакции.) Это улучшает параллельный одновременный конкурентный доступ к данным, но возможность неповторяемого чтения и фантомов продолжает существовать.
      Уровень изоляции READ COMMITTED для компонента Database Engine является уровнем изоляции по умолчанию.

    • Уровень изоляции REPEATABLE READ
      В отличие от уровня изоляции READ COMMITTED, уровень REPEATABLE READ устанавливает разделяемые блокировки на все считываемые данные и удерживает эти блокировки до тех пор, пока транзакция не будет подтверждена или отменена. Поэтому в этом случае многократное выполнение запроса внутри транзакции всегда будет возвращать один и тот же результат. Недостатком этого уровня изоляции является дальнейшее ухудшение одновременного конкурентного доступа, поскольку период времени, в течение которого другие транзакции не могут обновлять те же самые данные, значительно дольше, чем в случае уровня READ COMMITTED.
      Этот уровень изоляции не препятствует другим инструкциям вставлять новые строки, которые включаются в последующие операции чтения, вследствие чего могут появляться фантомы.

    • Уровень изоляции SERIALIZABLE
      Уровень изоляции SERIALIZABLE является самым строгим, потому что он не допускает возникновения всех четырех проблем параллельного одновременного конкурентного доступа, перечисленных ранее. Этот уровень устанавливает блокировку на всю область данных, считываемых соответствующей транзакцией. Поэтому этот уровень изоляции также предотвращает вставку новых строк другой транзакцией до тех пор, пока первая транзакция не будет подтверждена или отменена.
      Уровень изоляции SERIALIZABLE реализуется, используя метод блокировки диапазона ключа. Суть этого метода заключается в блокировке отдельных строк включительно со всем диапазоном строк между ними. Блокировка диапазона ключа блокирует элементы индексов, а не определенные страницы или всю таблицу. В этом случае любые операции модификации другой транзакцией невозможны, вследствие невозможности выполнения требуемых изменений элементов индекса.
      В заключение обсуждения четырех уровней изоляции следует упомянуть, что требуется знать, что чем выше уровень изоляции, тем меньше степень одновременного конкурентного доступа. Таким образом, уровень изоляции READ UNCOMMITTED меньше всего уменьшает одновременный конкурентный доступ. С другой стороны, он также предоставляет наименьшую изоляцию параллельных конкурентных транзакций. Уровень изоляции SERIALIZABLE наиболее сильно уменьшает степень одновременного конкурентного доступа, но гарантирует полную изоляцию параллельных конкурентных транзакций.

    • Уровни изоляции определяют, как транзакции взаимодействуют друг с другом. ACID — это аббревиатура, обозначающая свойства транзакций:

      • Atomicity (атомарность)
      • Consistency (согласованность)
      • Isolation (изоляция)
      • Durability (устойчивость)

  • Вопрос №7: [ Что такое агрегатные функции? ]

    Ответ
    • Агрегатные функции выполняют вычисления на наборе значений и возвращают одиночное значение.
    • Например: есть агрегатные функции, вычисляющие: count (количество), sum (сумму), avg (среднее), max (максимум) и min (минимум) для набора строк.

  • Вопрос №8: [ Чем агрегированные отличаются от оконных функций? ]

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

  • Вопрос №9: [ Партиционирование и шардинг, что такое и отличия? ]

    Ответ
    • Партиционирование (вертикальный шардинг) — это разделение одной таблицы на меньшие, но логически связанные части.
      • Партиционирование часто применяется внутри одной базы данных
    • Шардинг (горизонтальный шардинг) — это разбиение данных на разные базы или серверы.
      • Шардинг — на уровне всей системы хранения данных.

  • Вопрос №10: [ Что такое нормализация? ]

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

    • 1НФ (Первая нормальная форма) Таблица находится в 1НФ, если все колонки содержат атомарные, не разделимые значения, и у каждой строки есть уникальный идентификатор — первичный ключ.

    • 2НФ (Вторая нормальная форма) Таблица находится во 2НФ, если она уже в 1НФ и все ее колонки зависят от всего первичного ключа, а не от его части. Это особенно актуально для таблиц, в которых первичный ключ составной.

    • 3НФ (Третья нормальная форма) Таблица находится в 3НФ, если она в 2НФ и все атрибуты функционально зависят только от первичного ключа. То есть нет таких атрибутов, которые зависят от других неключевых атрибутов.

    • BCNF (Нормальная форма Бойса-Кодда) Это строже условие, чем 3НФ. Таблица находится в BCNF, если для каждой его нетривиальной функциональной зависимости X -> Y, X является суперключом.

    • 4НФ, 5НФ и далее Эти нормальные формы решают еще более специфические проблемы и обычно используются редко, в особо сложных базах данных.


Алгоритмы
  • Реализации: [ Реализации алгоритмов на языке Go (смотреть при необходимости) ]

    Реализации
    • Сортировка
      • Quick Sort
        • Quick Sort
          Сложность: O(n log n) в среднем и лучшем случае, O(n^2) в худшем случае.
          In-place: Да.
          Стабильность: Нет.

        • Когда использовать:
          Для больших наборов данных.
          Когда требуется быстрая "in-place" сортировка.
          Когда стабильность сортировки не является ключевым фактором.

        package main
        
        import "fmt"
        
        // quickSort сортирует подмассив arr[low:high] на месте.
        func quickSort(arr []int, low int, high int) {
            // Если индекс "low" больше или равен "high", прекратить выполнение.
            if low < high {
                // Находим индекс опорного элемента
                pivot := partition(arr, low, high)
                // Рекурсивно сортируем подмассивы слева и справа от опорного элемента
                quickSort(arr, low, pivot-1)
                quickSort(arr, pivot+1, high)
            }
        }
        
        // partition выбирает опорный элемент и перераспределяет элементы так, чтобы
        // элементы меньше опорного находились слева, а больше — справа.
        func partition(arr []int, low int, high int) int {
            // Используем последний элемент в качестве опорного
            pivot := arr[high]
            // Инициализируем i как индекс, указывающий на самый левый элемент
            i := low - 1
        
            // Проходим через каждый элемент и сравниваем его с опорным
            for j := low; j < high; j++ {
                if arr[j] <= pivot {
                    i++
                    // Меняем местами arr[i] и arr[j]
                    arr[i], arr[j] = arr[j], arr[i]
                }
            }
        
            // Помещаем опорный элемент на правильную позицию
            arr[i+1], arr[high] = arr[high], arr[i+1]
            return i + 1
        }
        
        func main() {
            arr := []int{9, 7, 5, 11, 12, 2, 14, 3, 10, 6}
            fmt.Println("Before sorting:", arr)
            quickSort(arr, 0, len(arr)-1)
            fmt.Println("After sorting:", arr)
        }
      • Merge Sort
        • Merge Sort
          Сложность: O(n log n) во всех случаях.
          In-place: Нет (требует дополнительную память).
          Стабильность: Да.

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

        package main
        
        import "fmt"
        
        // mergeSort рекурсивно разделяет и сортирует массив
        func mergeSort(arr []int) []int {
            // Если массив состоит из одного или нуля элементов, он уже отсортирован
            if len(arr) <= 1 {
              return arr
            }
        
          // Разделяем массив на две половины
          middle := len(arr) / 2
          left := mergeSort(arr[:middle])
          right := mergeSort(arr[middle:])
        
          // Сливаем отсортированные половины
          return merge(left, right)
        }
        
        // merge сливает два отсортированных массива в один отсортированный массив
        func merge(left, right []int) []int {
            // Инициализируем результат с предполагаемым размером для оптимизации
            result := make([]int, 0, len(left)+len(right))
        
          i, j := 0, 0
          // Проходим через каждый элемент в обоих массивах
          for i < len(left) && j < len(right) {
          	if left[i] <= right[j] {
          		result = append(result, left[i])
          		i++
          	} else {
          		result = append(result, right[j])
          		j++
          	}
          }
        
          // Добавляем оставшиеся элементы, если таковые есть
          result = append(result, left[i:]...)
          result = append(result, right[j:]...)
        
          return result
        }
        
        func main() {
            arr := []int{9, 7, 5, 11, 12, 2, 14, 3, 10, 6}
            fmt.Println("Before sorting:", arr)
            arr = mergeSort(arr)
            fmt.Println("After sorting:", arr)
        }
      • Bubble Sort
        • Bubble Sort
          Сложность: O(n^2)
          In-place: Да.
          Стабильность: Да.

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

        package main
        
        import "fmt"
        
        // bubbleSort сортирует массив на месте, используя алгоритм пузырьковой сортировки.
        func bubbleSort(arr []int) {
            n := len(arr)
        
          // Внешний цикл проходит по всему массиву
          for i := 0; i < n; i++ {
          	swapped := false // Флаг для оптимизации
        
          	// Внутренний цикл сравнивает каждую пару соседних элементов
          	for j := 0; j < n-i-1; j++ {
          		if arr[j] > arr[j+1] {
          			// Меняем местами
          			arr[j], arr[j+1] = arr[j+1], arr[j]
          			swapped = true
          		}
          	}
        
          	// Если не было обменов, массив уже отсортирован
          	if !swapped {
          		break
          	}
          }
        }
        
        func main() {
            arr := []int{64, 34, 25, 12, 22, 11, 90}
            fmt.Println("Before sorting:", arr)
            bubbleSort(arr)
            fmt.Println("After sorting:", arr)
        }
      • Insertion Sort
        • Insertion Sort
          Сложность: O(n^2)
          In-place: Да.
          Стабильность: Да.

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

        package main
        
        import "fmt"
        
        // insertionSort сортирует массив на месте, используя алгоритм сортировки вставками.
        func insertionSort(arr []int) {
            n := len(arr)
        
          // Внешний цикл перебирает все элементы массива
          for i := 1; i < n; i++ {
          	key := arr[i]  // Текущий элемент для вставки
          	j := i - 1 // Индекс предыдущего элемента
        
          	// Перемещаем все элементы, большие чем key, на одну позицию вперед
          	for j >= 0 && arr[j] > key {
          		arr[j+1] = arr[j]
          		j--
          	}
        
          	// Вставляем key на правильное место
          	arr[j+1] = key
          }
        }
        
        func main() {
            arr := []int{12, 11, 13, 5, 6}
            fmt.Println("Before sorting:", arr)
            insertionSort(arr)
            fmt.Println("After sorting:", arr)
        }
      • Heap Sort
        • Heap Sort
          Сложность: O(n log n)
          In-place: Да.
          Стабильность: Нет.

        • Когда использовать:
          Когда требуется быстрая сортировка без дополнительного использования памяти.
          Когда стабильность не требуется.

        package main
        
        import "fmt"
        
        // heapify - функция для преобразования поддерева в двоичную кучу
        // с корнем в индексе i для данного массива arr длины n
        func heapify(arr []int, n int, i int) {
            largest := i          // Инициализируем largest как корень
            l := 2*i + 1          // Левый = 2*i + 1
            r := 2*i + 2          // Правый = 2*i + 2
        
          // Если левый дочерний элемент больше корня
          if l < n && arr[l] > arr[largest] {
          	largest = l
          }
        
          // Если правый дочерний элемент больше корня
          if r < n && arr[r] > arr[largest] {
          	largest = r
          }
        
          // Если largest не корень
          if largest != i {
          	arr[i], arr[largest] = arr[largest], arr[i]
        
          	// Рекурсивно преобразуем в кучу поддерево с корнем в largest
          	heapify(arr, n, largest)
          }
        }
        
        // heapSort - функция для сортировки массива длины n
        func heapSort(arr []int, n int) {
            // Построение кучи (перегруппировка массива)
            for i := n / 2 - 1; i >= 0; i-- {
            heapify(arr, n, i)
            }
        
          // Один за другим извлекаем элементы из кучи
          for i := n - 1; i >= 0; i-- {
          	// Перемещаем текущий корень в конец
          	arr[0], arr[i] = arr[i], arr[0]
        
          	// Вызываем heapify на уменьшенной куче
          	heapify(arr, i, 0)
          }
        }
        
        func main() {
            arr := []int{12, 11, 13, 5, 6, 7}
            n := len(arr)
          fmt.Println("Before sorting:", arr)
          heapSort(arr, n)
          fmt.Println("After sorting:", arr)
        }

    • Поиск
      • Binary Search
        • Binary Search (Бинарный поиск)
          Сложность: O(log n) в среднем и лучшем случае, где n - размер массива, O(1) в худшем случае (когда элемент не найден). In-place: Да. Стабильность: Нет.

        • Когда использовать: Когда массив упорядочен и требуется найти элемент. При работе с большими отсортированными данными, где бинарный поиск эффективнее линейного поиска.

        package main
        
        import "fmt"
        
        // binarySearch выполняет бинарный поиск элемента в упорядоченном массиве.
        func binarySearch(arr []int, target int) int {
            low, high := 0, len(arr)-1
        
          for low <= high {
          	mid := low + (high-low)/2 // Находим средний индекс
        
          	if arr[mid] == target {
          		return mid // Найден элемент, возвращаем его индекс
          	} else if arr[mid] < target {
          		low = mid + 1 // Искомый элемент находится в правой половине
          	} else {
          		high = mid - 1 // Искомый элемент находится в левой половине
          	}
          }
        
          return -1 // Элемент не найден
        }
        
        func main() {
            arr := []int{2, 4, 6, 8, 10, 12, 14, 16}
            target := 10
            index := binarySearch(arr, target)
        
          if index != -1 {
          	fmt.Printf("%d found at index %d\n", target, index)
          } else {
          	fmt.Printf("%d not found in the array\n", target)
          }
        }
      • Linear Search
        • Linear Search (Линейный поиск) Сложность: O(n) в среднем, лучшем и худшем случае, где n - размер массива. In-place: Да. Стабильность: Нет

        • Когда использовать: Когда массив небольшой или не упорядочен. Для поиска элемента в небольших наборах данных. Когда другие алгоритмы не применимы или неэффективны из-за малого размера данных.

        package main
        
        import "fmt"
        
        // linearSearch выполняет линейный поиск элемента в массиве.
        func linearSearch(arr []int, target int) int {
            for i, num := range arr {
                if num == target {
                    return i // Найден элемент, возвращаем его индекс
                }
            }
            return -1 // Элемент не найден
        }
        
        func main() {
            arr := []int{2, 4, 6, 8, 10, 12, 14, 16}
            target := 10
            index := linearSearch(arr, target)
        
          if index != -1 {
          	fmt.Printf("%d found at index %d\n", target, index)
          } else {
          	fmt.Printf("%d not found in the array\n", target)
          }
        }
      • Depth-First Search (DFS)
        • Depth-First Search (DFS поиск в глубину) Сложность: O(V + E), где V - количество вершин, E - количество рёбер. In-place: Нет. Стабильность: Нет.

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

        package main
        
        import (
        "fmt"
        )
        
        // Graph: представляет собой граф в виде карты, где ключ - это вершина, а значение - это список смежных вершин.
        type Graph map[int][]int
        
        // DFS: обходит граф в глубину начиная с вершины start.
        func DFS(graph Graph, start int, visited map[int]bool) {
            // Отмечаем текущую вершину как посещенную
            visited[start] = true
            fmt.Println(start)
        
          // Перебираем всех соседей текущей вершины
          for _, neighbor := range graph[start] {
          	if !visited[neighbor] { // Если сосед не был посещен
          		DFS(graph, neighbor, visited) // Рекурсивно вызываем DFS для соседа
          	}
          }
        }
        
        func main() {
            graph := Graph{
            0: {1, 2},
            1: {2},
            2: {0, 3},
            3: {3},
            }
        
          visited := make(map[int]bool)
        
          fmt.Println("Depth-First Search:")
          DFS(graph, 2, visited)
        }
      • Breadth-First Search (BFS)
        • Breadth-First Search (BFS поиск в ширину) Сложность: O(V + E), где V - количество вершин, E - количество рёбер. In-place: Нет. Стабильность: Нет.

        • Когда использовать: Для поиска кратчайшего пути в графе. Когда нужно исследовать все соседние вершины на текущем уровне графа (например, в задачах поиска кратчайшего пути). Когда нужен полный обход графа в ширину.

        package main
        
        import (
            "fmt"
            "container/list"
        )
        
        // Graph представляет собой граф в виде карты, где ключ - это вершина, а значение - это список смежных вершин.
        type Graph map[int][]int
        
        // BFS обходит граф в ширину начиная с вершины start.
        func BFS(graph Graph, start int) {
            // Создаем очередь и добавляем в нее стартовую вершину
            queue := list.New()
            queue.PushBack(start)
        
          // Инициализируем множество посещенных вершин
          visited := make(map[int]bool)
          visited[start] = true
        
          fmt.Println("Breadth-First Search:")
        
          for queue.Len() > 0 {
          	// Извлекаем вершину из начала очереди
          	element := queue.Front()
          	vertex := element.Value.(int)
          	queue.Remove(element)
        
          	fmt.Println(vertex)
        
          	// Добавляем все непосещенные соседние вершины в очередь
          	for _, neighbor := range graph[vertex] {
          		if !visited[neighbor] {
          			queue.PushBack(neighbor)
          			visited[neighbor] = true
          		}
          	}
          }
        }
        
        func main() {
            graph := Graph{
            0: {1, 2},
            1: {2},
            2: {0, 3},
            3: {3},
            }
        
        	fmt.Println("Breadth-First Search:")
        	BFS(graph, 2)
        }

    • Динамическое программирование
      • Fibonacci Series
        • Сложность: O(n) в среднем, лучшем и худшем случае. In-place: Да (использует дополнительное пространство размером O(n)). Стабильность: Не применимо.

        • Когда использовать: Когда требуется вычислить n-ое число Фибоначчи.

        package main
        
        import "fmt"
        
        // fibonacci использует динамическое программирование для вычисления n-го числа Фибоначчи
        func fibonacci(n int) int {
            // Если n равно 0 или 1, возвращаем само число
            if n <= 1 {
                return n
            }
            // Инициализация слайса для сохранения чисел Фибоначчи
            fib := make([]int, n+1)
            fib[1] = 1
            // Вычисляем числа Фибоначчи начиная с 2 до n
            for i := 2; i <= n; i++ {
                fib[i] = fib[i-1] + fib[i-2]
            }
            return fib[n]
        }
        
        func main() {
            n := 10 // Можно изменить на любое желаемое число
            fmt.Println(fibonacci(n))
        }
      • Knapsack Problem
        • Сложность: O(nW) в среднем, лучшем и худшем случае, где n - количество предметов, а W - вместимость рюкзака. In-place: Нет. Стабильность: Не применимо.

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

        package main
        
        import "fmt"
        
        // Вспомогательная функция для получения максимального значения
        func max(a, b int) int {
            if a > b {
                return a
            }
            return b
        }
        
        // 0-1 knapsack problem решение задачи о рюкзаке
        func knapsack(values, weights []int, capacity int) int {
            n := len(values)
            // Инициализация двумерного слайса для динамического программирования
            dp := make([][]int, n+1)
            for i := range dp {
                dp[i] = make([]int, capacity+1)
            }
        
            // Заполнение таблицы dp
            for i := 1; i <= n; i++ {
                for w := 1; w <= capacity; w++ {
                    if weights[i-1] <= w {
                        dp[i][w] = max(dp[i-1][w], values[i-1]+dp[i-1][w-weights[i-1]])
                    } else {
                        dp[i][w] = dp[i-1][w]
                    }
                }
            }
            return dp[n][capacity]
        }
        
        func main() {
            values := []int{60, 100, 120}  // Значения предметов
            weights := []int{10, 20, 30}  // Веса предметов
            capacity := 50                // Вместимость рюкзака
            fmt.Println(knapsack(values, weights, capacity))
        }
      • Longest Common Subsequence
        • Сложность: O(mn) в среднем, лучшем и худшем случае, где m и n - длины двух строк. In-place: Нет. Стабильность: Не применимо.

        • Когда использовать: Когда нужно определить наибольшую общую подпоследовательность между двумя строками.

        package main
        
        import "fmt"
        
        // Вспомогательная функция для получения максимального значения
        func max(a, b int) int {
            if a > b {
                return a
            }
            return b
        }
        
        // lcs находит наибольшую общую подпоследовательность двух строк
        func lcs(X, Y string) int {
            m := len(X)
            n := len(Y)
            // Инициализация двумерного слайса для динамического программирования
            dp := make([][]int, m+1)
            for i := range dp {
                dp[i] = make([]int, n+1)
            }
        
            // Заполнение таблицы dp
            for i := 1; i <= m; i++ {
                for j := 1; j <= n; j++ {
                    if X[i-1] == Y[j-1] {
                        dp[i][j] = dp[i-1][j-1] + 1
                    } else {
                        dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                    }
                }
            }
            return dp[m][n]
        }
        
        func main() {
            str1 := "AGGTAB"
            str2 := "GXTXAYB"
            fmt.Println(lcs(str1, str2))
        }
      • Coin Change Problem
        • Сложность: O(amount * n) в среднем, лучшем и худшем случае, где n - количество различных монет. In-place: Да (использует дополнительное пространство размером O(amount)). Стабильность: Не применимо.

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

        package main
        
        import (
            "fmt"
            "math"
        )
        
        // coinChange решает задачу о размене монет
        func coinChange(coins []int, amount int) int {
            // Инициализация слайса для динамического программирования
            dp := make([]int, amount+1)
            for i := range dp {
                dp[i] = math.MaxInt32
            }
            dp[0] = 0
        
            // Заполнение таблицы dp
            for _, coin := range coins {
                for i := coin; i <= amount; i++ {
                    dp[i] = min(dp[i], dp[i-coin]+1)
                }
            }
        
            if dp[amount] == math.MaxInt32 {
                return -1
            }
            return dp[amount]
        }
        
        // Вспомогательная функция для получения минимального значения
        func min(a, b int) int {
            if a < b {
                return a
            }
            return b
        }
        
        func main() {
            coins := []int{1, 2, 5}
            amount := 11
            fmt.Println(coinChange(coins, amount))
        }

    • Графы
      • Dijkstra's Algorithm
        • Сложность:
          В среднем: O(V*V), но с приоритетной очередью или кучей:O(V + E logV).
          Лучший случай: O(V+ElogV) с кучей
          In-place: Да.
          Стабильность: Не применимо.
        • Когда использовать: Когда требуется найти кратчайшие пути от одной начальной вершины ко всем другим вершинам в взвешенном графе.
        package main
        
        import (
            "fmt"
            "math"
        )
        
        // Определение константы для представления бесконечности
        const Infinity = math.MaxInt64
        
        // Функция для вычисления кратчайших путей от начальной вершины до всех остальных
        func dijkstra(graph [][]int, start int) []int {
            n := len(graph)
            visited := make([]bool, n) // массив для отметки посещенных вершин
            dist := make([]int, n)     // массив для хранения расстояния от стартовой вершины до каждой другой
            for i := 0; i < n; i++ {
                dist[i] = Infinity // инициализация расстояний как бесконечности
            }
            dist[start] = 0 // расстояние до стартовой вершины равно 0
        
          for i := 0; i < n-1; i++ {
          	u := minDistance(dist, visited) // выбор вершины с минимальным расстоянием из непосещенных
          	visited[u] = true                // помечаем эту вершину как посещенную
        
          	// Обновление значений расстояния для соседних вершин выбранной вершины
          	for v := 0; v < n; v++ {
          		// Проходим только по непосещенным вершинам с ненулевым весом ребра и обновляем расстояние, если найден более короткий путь
          		if !visited[v] && graph[u][v] != 0 && dist[u] != Infinity && dist[u]+graph[u][v] < dist[v] {
          			dist[v] = dist[u] + graph[u][v]
          		}
          	}
          }
          return dist
        }
        
        // Вспомогательная функция для нахождения вершины с минимальным расстоянием из непосещенных
        func minDistance(dist []int, visited []bool) int {
            minim := Infinity // начальное значение минимального расстояния
            minIndex := -1  // индекс вершины с минимальным расстоянием
        
          for i := 0; i < len(dist); i++ {
          	if dist[i] < minim && !visited[i] {
          		minim = dist[i]
          		minIndex = i
          	}
          }
          return minIndex
        }
        
        func main() {
            graph := [][]int{
            // представление графа в виде матрицы смежности
            {0, 4, 0, 0, 0, 0, 0, 8, 0},
            // ... (и так далее для каждой строки матрицы)
            }
            // Вывод результатов работы алгоритма Дейкстры для стартовой вершины 0
            fmt.Println(dijkstra(graph, 0))
        }
      • Floyd-Warshall Algorithm
        • Сложность:
          В среднем: O(V * V * V)
          Лучший случай: O(V * V * V)
          Худший случай: O(V * V * V)
          In-place: Да.
          Стабильность: Не применимо.
        • Когда использовать: Когда нужно найти кратчайшие пути между всеми парами вершин в графе.
        package main
        
        import (
            "fmt"
            "math"
        )
        
        // Инициализация констант для представления бесконечности
        const Infinity = math.MaxInt64
        const NULL = -1
        
        // Функция реализующая алгоритм Флойда-Уоршалла для нахождения кратчайших путей между всеми парами вершин
        func floydWarshall(graph [][]int) [][]int {
            n := len(graph)
            dist := make([][]int, n) // матрица для хранения кратчайших путей между вершинами
            for i := 0; i < n; i++ {
                dist[i] = make([]int, n)
                for j := 0; j < n; j++ {
                    if i == j {
                        dist[i][j] = 0 // расстояние от вершины до самой себя равно 0
                    } else if graph[i][j] != 0 {
                        dist[i][j] = graph[i][j] // если есть прямое ребро, используем его вес
                    } else {
                        dist[i][j] = Infinity // иначе устанавливаем бесконечность
                    }
                }
        }
        
          // Проход по всем вершинам и обновление кратчайших путей
          for k := 0; k < n; k++ {
          	for i := 0; i < n; i++ {
          		for j := 0; j < n; j++ {
          			// Если путь через вершину k короче, чем текущий путь, обновляем значение
          			if dist[i][k]+dist[k][j] < dist[i][j] {
          				dist[i][j] = dist[i][k] + dist[k][j]
          			}
          		}
          	}
          }
          return dist
        }
        
        func main() {
            graph := [][]int{
            // представление графа в виде матрицы смежности
            {0, 5, Infinity, 10},
            // ... (и так далее для каждой строки матрицы)
            }
            // Вывод результатов работы алгоритма Флойда-Уоршалла
            result := floydWarshall(graph)
            for _, row := range result {
                fmt.Println(row)
            }
        }
      • Kruskal's Algorithm
        • Сложность:
          В среднем: O(E logE) или O(E logV)
          Лучший случай: O(E logE) или O(E logV)
          Худший случай: O(E logE) или O(E logV)
          In-place: Нет.
          Стабильность: Не применимо.
        • Когда использовать: Для нахождения минимального остовного дерева во взвешенном графе.
        package main
        
        import (
            "fmt"
            "sort"
        )
        
        type Edge struct {
        src, dest, weight int
        }
        
        // Для создания и управления множествами
        type Subset struct {
        parent, rank int
        }
        
        // Находит представителя множества элемента i
        func find(subsets []Subset, i int) int {
            if subsets[i].parent != i {
                subsets[i].parent = find(subsets, subsets[i].parent)
            }
            return subsets[i].parent
        }
        
        // Объединяет два множества по рангу
        func union(subsets []Subset, x, y int) {
            xroot := find(subsets, x)
            yroot := find(subsets, y)
        
          // Прикрепляем дерево меньшего ранга к корню дерева с большим рангом
          if subsets[xroot].rank < subsets[yroot].rank {
          	subsets[xroot].parent = yroot
          } else if subsets[xroot].rank > subsets[yroot].rank {
          	subsets[yroot].parent = xroot
          } else {
          	subsets[yroot].parent = xroot
          	subsets[xroot].rank++
          }
        }
        
        // Алгоритм Краскала для поиска минимального остовного дерева
        func kruskal(edges []Edge, vertices int) []Edge {
            sort.Slice(edges, func(i, j int) bool {
            return edges[i].weight < edges[j].weight
            }) // Сортируем ребра по весу
        
          subsets := make([]Subset, vertices)
          for i := 0; i < vertices; i++ {
          	subsets[i] = Subset{parent: i, rank: 0}
          }
        
          var mst []Edge
          e := 0 // Индекс для mst[]
        
          for _, edge := range edges {
          	x := find(subsets, edge.src)
          	y := find(subsets, edge.dest)
        
          	if x != y { // Если это не формирует цикл
          		mst = append(mst, edge)
          		union(subsets, x, y)
          		e++
          	}
          	if e == vertices-1 {
          		break
          	}
          }
          return mst
        }
      • Prim's Algorithm
        • Сложность:
          В среднем: O(V * V)
          Лучший случай: O(E+VlogV) c кучей
          Худший случай: O(V * V) In-place: Нет.
          Стабильность: Не применимо.
        • Когда использовать: Для нахождения минимального остовного дерева во взвешенном графе.
        package main
        
        import (
          "fmt"
          "math"
        )
        
        // Определение константы для представления бесконечности
        const Infinity = math.MaxInt64
        
        // Функция для нахождения вершины с минимальным ключом из множества вершин, не включенных в MST
        func minKey(key []int, mstSet []bool) int {
          minim := Infinity
          minIndex := 0
          
          for v := 0; v < len(key); v++ {
              if mstSet[v] == false && key[v] < minim {
          	    minim = key[v]
          	    minIndex = v
              }
          }
          return minIndex
        }
        
        // Функция для вывода ребер и их веса минимального остовного дерева
        func printMST(parent []int, graph [][]int) {
          for i := 1; i < len(graph); i++ { 
                fmt.Printf("Ребро %d - %d \t Вес: %d \n", parent[i], i, graph[i][parent[i]])
            }
        }
        
        // Функция для построения и вывода MST для графа, представленного в виде матрицы смежности
        func primMST(graph [][]int) {
            n := len(graph)
            key := make([]int, n)   // Значения, используемые для выбора минимального ребра для каждой вершины
            parent := make([]int, n) // Array to store constructed MST
            mstSet := make([]bool, n)
            
            // Инициализация всех ключей как бесконечность
            for i := 0; i < n; i++ {
                key[i] = Infinity
            }
            
            key[0] = 0     // Делаем ключ 0 так, чтобы эта вершина была выбрана первой
            parent[0] = -1 // Первый узел всегда корень нашего MST
            
            for count := 0; count < n-1; count++ {
                // Выбираем минимальный ключ из множества вершин, которые еще не включены в MST
                u := minKey(key, mstSet)
                mstSet[u] = true // Добавляем выбранную вершину в mstSet
                
                // Обновляем значения ключа и индекса родителя для вершин, смежных с выбранной вершиной
                for v := 0; v < n; v++ {
                    if graph[u][v] != 0 && mstSet[v] == false && graph[u][v] < key[v] {
                        parent[v] = u
                        key[v] = graph[u][v]
                    }
                }
          }
        
            printMST(parent, graph)
        }
        
        func main() {
            graph := [][]int{
                {0, 2, 0, 6, 0},
                {2, 0, 3, 8, 5},
                {0, 3, 0, 0, 7},
                {6, 8, 0, 0, 9},
                {0, 5, 7, 9, 0},
            }
            primMST(graph)
        }

    • Строки
      • KMP Algorithm
        • Сложность:
          В среднем: O(n+m)
          Лучший случай: O(n) (если подстрока отсутствует)
          Худший случай: O(n+m)
          In-place: Да
          Стабильность: Не применимо (это алгоритм поиска)
        • Когда использовать: Когда нужен быстрый поиск подстроки в строке.
        package main
        
        import (
            "fmt"
        )
        
        // Функция для вычисления префикс-функции для KMP
        func computeLPSArray(pattern string) []int {
            length := 0
            lps := make([]int, len(pattern))
        
            i := 1
            // Проход по всей строке для вычисления lps[i]
            for i < len(pattern) {
                if pattern[i] == pattern[length] {
                    length++
                    lps[i] = length
                    i++
                } else {
                    if length != 0 {
                        // Найдено несовпадение, следует уменьшить length
                        length = lps[length-1]
                    } else {
                        lps[i] = 0
                        i++
                    }
                }
            }
            return lps
        }
        
        // Функция KMP поиска
        func KMPSearch(pat, txt string) {
            m := len(pat)
            n := len(txt)
            lps := computeLPSArray(pat)
        
            i, j := 0, 0
            // Проход по главной строке
            for i < n {
        	    if pat[j] == txt[i] {
        	        i++
        	        j++
        	    }
        	    // Если найдено полное совпадение
        	    if j == m {
        	        fmt.Printf("Найден шаблон на индексе %d\n", i-j)
        	        j = lps[j-1]
        	    } else if i < n && pat[j] != txt[i] {
        	        if j != 0 {
        	            j = lps[j-1]
        	        } else {
        	            i++
        	        }
        	    }
        	}
        }
        
        func main() {
            txt := "ABABDABACDABABCABAB"
            pat := "ABABCABAB"
            KMPSearch(pat, txt)
        }
      • Rabin-Karp Algorithm
        • Сложность:
          В среднем: O(n+m)
          Лучший случай: O(n+m)
          Худший случай: O(nm) при плохом хеше
          In-place: Да
          Стабильность: Не применимо (это алгоритм поиска)
        • Когда использовать: Когда нужен алгоритм на основе хеширования для поиска подстроки.
        package main
        
        import (
            "fmt"
        )
        
        const base = 256 // основание для хеширования
        const prime = 101 // простое число для модуляции хеша
        
        // Функция поиска с использованием Rabin-Karp
        func rabinKarpSearch(pat, txt string) {
            m := len(pat)
            n := len(txt)
            i, j := 0, 0
            pHash, tHash := 0, 0
            h := 1
        
            // Вычисление h^(m-1) для последующего использования
            for i = 0; i < m-1; i++ {
                h = (h * base) % prime
            }
        
            // Вычисление хешей для pat и первого окна txt
            for i = 0; i < m; i++ {
                pHash = (base*pHash + int(pat[i])) % prime
                tHash = (base*tHash + int(txt[i])) % prime
            }
        
            // Скользящее окно по тексту txt
            for i = 0; i <= n-m; i++ {
                // Сравнение хешей текущего окна txt и pat
                if pHash == tHash {
                    // При совпадении хешей сравниваем символы
                    for j = 0; j < m; j++ {
                        if txt[i+j] != pat[j] {
                            break
                        }
                    }
                    // Если pat[0...m-1] = txt[i, i+1, ...i+m-1]
                    if j == m {
                        fmt.Printf("Найден шаблон на индексе %d\n", i)
                    }
                }
        
                // Вычисление хеша для следующего окна txt
                if i < n-m {
                    tHash = (base*(tHash-int(txt[i])*h) + int(txt[i+m])) % prime
                    if tHash < 0 {
                        tHash += prime
                    }
                }
            }
        }
        
        func main() {
            txt := "ABABDABACDABABCABAB"
            pat := "ABABCABAB"
            rabinKarpSearch(pat, txt)
        }
      • Z-Algorithm
        • Сложность:
          В среднем: O(n+m)
          Лучший случай: O(n+m)
          Худший случай: O(n+m)
          In-place: Да
          Стабильность: Не применимо (это алгоритм поиска)
        • Когда использовать: Когда нужен алгоритм для поиска подстроки, основанный на Z-массиве.
        package main
        
        import (
            "fmt"
        )
        
        // Функция для вычисления Z-массива
        func getZArray(str string) []int {
            n := len(str)
            z := make([]int, n)
        
            l, r, k := 0, 0, 0
            for i := 1; i < n; i++ {
                // Если i находится за пределами текущего Z-окна, вычисляем Z[i] "с нуля"
                if i > r {
                    l, r = i, i
                    for r < n && str[r-l] == str[r] {
                        r++
                    }
                    z[i] = r - l
                    r--
                } else {
                    // Иначе применяем оптимизацию к вычислению Z[i]
                    k = i - l
                    if z[k] < r-i+1 {
                        z[i] = z[k]
                    } else {
                        l = i
                        for r < n && str[r-l] == str[r] {
                            r++
                        }
                        z[i] = r - l
                        r--
                    }
                }
            }
            return z
        }
        
        // Функция для поиска с использованием Z-алгоритма
        func zSearch(text, pattern string) {
            // Конкатенация текста и паттерна
            concat := pattern + "$" + text
            z := getZArray(concat)
            // Обход полученного Z-массива
            for i := 0; i < len(z); i++ {
                // Если значение z[i] равно длине паттерна, значит, паттерн найден на этой позиции
                if z[i] == len(pattern) {
                    fmt.Printf("Найден шаблон на индексе %d\n", i-len(pattern)-1)
                }
            }
        }
        
        func main() {
            txt := "ABABDABACDABABCABAB"
            pat := "ABABCABAB"
            zSearch(txt, pat)
        }

    • Хэширование
      • Hashing Algorithms
        • Реализации алгоритмов хеширования очень объемны. По этому рекомендую ознакомиться с ними самостоятельно


  • Вопрос №1: [ Какие часто используемые алгоритмы ты знаешь? ]

    Ответ
    • Сортировка
      • Quick Sort — быстрая сортировка
      • Merge Sort — сортировка слиянием
      • Bubble Sort — сортировка пузырьком
      • Insertion Sort — сортировка вставками
      • Heap Sort — сортировка кучей
    • Поиск
      • Binary Search — двоичный поиск
      • Linear Search — линейный поиск
      • Depth-First Search (DFS) — поиск в глубину
      • Breadth-First Search (BFS) — поиск в ширину
    • Динамическое программирование
      • Fibonacci Series — вычисление чисел Фибоначчи
      • Knapsack Problem — задача о рюкзаке
      • Longest Common Subsequence — наибольшая общая подпоследовательность
      • Coin Change Problem — задача о размене монет
    • Графы
      • Dijkstra's Algorithm — алгоритм Дейкстры для нахождения кратчайшего пути
      • Floyd-Warshall Algorithm — алгоритм Флойда—Уоршелла
      • Kruskal's Algorithm — алгоритм Краскала для минимального остовного дерева
      • Prim's Algorithm — алгоритм Прима для минимального остовного дерева
    • Строки
      • KMP Algorithm — алгоритм Кнута—Морриса—Пратта для поиска подстроки
      • Rabin-Karp Algorithm — алгоритм Рабина—Карпа для поиска подстроки
      • Z-Algorithm — для поиска подстроки
    • Хеширование
      • MD5 (Message Digest Algorithm 5): Производит 128-битный хеш.
        Широко использовался для проверки целостности данных.
        Сейчас считается устаревшим из-за потенциальных уязвимостей.
      • SHA (Secure Hash Algorithms):
        • SHA-0: Первоначальная версия, быстро была заменена из-за наличия уязвимостей.
        • SHA-1: Производит 160-битный хеш. В настоящее время считается устаревшим из-за возможности коллизий.
        • SHA-2: Семейство функций, включая SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256. Это более безопасные варианты по сравнению с SHA-1.
        • SHA-3: Последний и наиболее современный член семейства Secure Hash Algorithms. Отличается от SHA-2.
      • CRC32 (Cyclic Redundancy Check):
        Используется в сетевых коммуникациях для обнаружения изменений в сырых данных.
      • bcrypt:
        Основан на Blowfish-шифровании.
        Широко используется для хеширования паролей из-за его возможности "соления" и адаптивной природы.
      • scrypt:
        Используется для хеширования паролей.
        Основан на потреблении памяти, что делает его устойчивым к атакам на основе специализированного оборудования.
      • argon2:
        Победитель Password Hashing Competition в 2015 году.
        Имеет несколько вариантов: argon2i (для устойчивости к атакам на основе времени памяти) и argon2d (для устойчивости к GPU-атакам).
      • Murmur и CityHash:
        Не предназначены для криптографического использования.
        Быстро генерируют хеш для использования в таких структурах данных, как хеш-таблицы.
      • BLAKE2:
        Криптографическая хеш-функция, которая быстрее, чем MD5, SHA-1 и SHA-2.
      • Whirlpool:
        Криптографическая функция, производящая 512-битный хеш.
      • RIPEMD (RACE Integrity Primitives Evaluation Message Digest):

  • Вопрос №2: [ Как отсортировать файл на 100GB с 1GB ОЗУ? ]

    Ответ
    • Используйте внешнюю сортировку:
    • Разделите большой файл на меньшие части размером < 1GB.
    • Отсортируйте каждую часть в памяти и сохраните на диск.
    • Объедините отсортированные части, считывая и сравнивая первые элементы каждого файла.

DevOps
  • Управление конфигурацией
    • Вопрос №1: [ Какие системы управления конфигурацией знаешь? ]

      Ответ
      • Ansible
      • Puppet
      • Chef
      • SaltStack

    • Вопрос №2: [ Что такое Ansible, плюсы и минусы? ]

      Ответ
      • Ansible — это инструмент для автоматизации, который использует "Playbooks" для описания автоматизации на языке YAML. Он не требует установки агента на управляемых серверах, используя вместо этого SSH для выполнения команд и копирования ресурсов. Ansible хорошо подходит для комплексных сред с множеством разных типов операций и систем.
      • Плюсы:
        • Простота и доступность: Использует YAML для создания Playbooks, что делает его легким для чтения и написания.
        • Безагентный: Не требует установки агентов на управляемых машинах; использует SSH для коммуникации.
        • Модульность: Большая библиотека модулей, поддерживает множество типов операций и систем.
        • Интеграция с другими инструментами: Хорошо интегрируется с другими DevOps-инструментами, такими как Jenkins, AWS и Docker.
      • Минусы:
        • Производительность: В некоторых случаях может быть медленнее, чем агентные решения, особенно на больших сетях.
        • Ограниченный язык: YAML может быть не таким мощным или гибким, как DSL в других инструментах.

    • Вопрос №3: [ Что такое Puppet, плюсы и минусы? ]

      Ответ
      • Puppet — это инструмент для управления конфигурациями, который использует собственный декларативный язык для описания состояния ресурсов. Puppet обычно использует агента на управляемых серверах, которые регулярно сверяются с "мастер"-сервером для обновления своей конфигурации.
      • Плюсы:
        • Зрелость: Один из самых старых и зрелых инструментов для управления конфигурацией.
          • Мощный DSL: Специализированный язык для описания ресурсов и зависимостей между ними.
        • Большое сообщество: Обширная база пользователей и модулей.
      • Минусы:
        • Сложность: Из-за своей мощи и гибкости может быть сложным для новичков.
        • Необходимость агента: Требует установки агента на каждом управляемом сервере.

    • Вопрос №4: [ Что такое Chef, плюсы и минусы? ]

      Ответ
      • Chef также предназначен для автоматизации управления конфигурациями и использует Ruby-подобный DSL (domain-specific language) для описания "рецептов", определяющих, как должны быть сконфигурированы ресурсы. Как и Puppet, Chef обычно использует агент-серверную архитектуру, где агенты на управляемых серверах периодически связываются с сервером для обновления конфигурации.
      • Плюсы:
        • Гибкий DSL: На основе Ruby, предоставляет большую гибкость для написания сложных конфигураций.
        • Тестирование и разработка: Поддержка тестирования кода перед развертыванием.
        • Широкая экосистема: Обширный набор интеграций, книг рецептов и модулей.
      • Минусы:
        • Сложность: Мощный, но сложный язык и большое количество возможностей могут быть перегрузкой для новичков.
        • Агент на сервере: Также как и Puppet, требует установки агента

    • Вопрос №5: [ Что такое SaltStack, плюсы и минусы? ]

      Ответ
      • Salt — еще один инструмент для управления конфигурациями и автоматизации, который использует декларативный язык на основе YAML или JSON. Он обеспечивает очень быстрое распространение команд и конфигураций, используя "ZeroMQ" для связи между мастер-сервером и управляемыми системами.
      • Плюсы:
        • Высокая производительность: Очень быстрое распространение команд, благодаря использованию ZeroMQ.
        • Гибкость: Поддерживает различные моды работы, включая агентный и безагентный.
        • Набор функций: Помимо управления конфигурациями, предлагает возможности для оркестровки и мониторинга.
      • Минусы:
        • Сложность: Из-за множества функций и настроек, кривая обучения может быть высокой.
        • Документация: Хотя документация обширна, она иногда может быть неполной или запутанной.

  • Контейнеризация и оркестрация
    • Вопрос №1: [ Что такое система оркестрации контейнеров? ]

      Ответ
      • Системы оркестрации контейнеров, такие как Kubernetes, Docker Swarm или Mesos, используются для автоматизации развертывания, масштабирования и управления контейнеризованными приложениями.

        Для чего они нужны:

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

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


    • Вопрос №2: [ Что такое система контейнеризации? ]

      Ответ
      • Система контейнеризации — это метод автоматизации развертывания, упаковки и управления приложениями в легковесных, портативных "контейнерах". Эти контейнеры можно легко перемещать между различными средами и хостами, что обеспечивает гибкость и упрощает процесс разработки, тестирования и распределения приложений.
      • Основные характеристики:
        • Изоляция ресурсов: Контейнеры изолированы друг от друга и используют свои файловые системы, сетевые стеки и процессы, что улучшает безопасность и упрощает управление зависимостями.
        • Портативность: Контейнеры включают в себя все, что необходимо для работы приложения, что делает их портативными и легкими для развертывания на различных платформах и окружениях.
        • Эффективность: Контейнеры сильно оптимизированы для использования системных ресурсов, таких как процессор, память и дисковое пространство, что делает их более эффективными по сравнению с традиционными виртуальными машинами.
        • Масштабируемость и оркестровка: Системы контейнеризации часто совместимы с инструментами оркестровки, такими как Kubernetes, которые автоматизируют развертывание, масштабирование и управление контейнерами.
        • Декларативная конфигурация: В большинстве систем контейнеризации конфигурации описываются декларативно, что упрощает процесс развертывания и управления.

    • Вопрос №3: [ Какие системы контейнеризации и оркестрации знаешь? ]

      Ответ
      • Docker
      • Kubernetes
      • Docker Swarm
      • OpenShift
      • Mesos

    • Вопрос №4: [ Docker, плюсы и минусы? ]

      Ответ
      • Плюсы:
        • Портативность: Все зависимости приложения упакованы в контейнер, что облегчает перенос между различными средами.
        • Изоляция: Контейнеры изолированы друг от друга, что улучшает безопасность.
        • Легковесность: Меньше накладных расходов по сравнению с традиционными виртуальными машинами.
        • Масштабируемость: Легко масштабировать и распределять с помощью оркестровщиков, таких как Kubernetes.
        • Экосистема и сообщество: Обширная библиотека готовых образов и большое сообщество.
      • Минусы:
        • Сложность: Может быть сложно для новичков.
        • Безопасность: По умолчанию контейнеры имеют определенные уровни изоляции, которые могут быть недостаточными для некоторых приложений.

    • Вопрос №5: [ Kubernetes, плюсы и минусы? ]

      Ответ
      • Плюсы:
        • Масштабируемость: Отлично подходит для управления большим числом контейнеров.
        • Самовосстановление: Автоматически перезапускает контейнеры при сбоях.
        • Балансировка нагрузки: Встроенная балансировка нагрузки для распределения трафика.
        • Экосистема: Множество дополнительных инструментов и большое сообщество.
      • Минусы:
        • Сложность: Высокий порог входа и сложная архитектура.
        • Ресурсоемкость: Требует больших вычислительных ресурсов для запуска.

    • Вопрос №6: [ Docker Swarm, плюсы и минусы? ]

      Ответ
      • Плюсы:
        • Простота: Проще в установке и использовании по сравнению с Kubernetes.
        • Интеграция с Docker: Тесная интеграция с Docker обеспечивает легкость в управлении.
        • Масштабируемость: Хорошо масштабируется, но менее мощно, чем Kubernetes.
      • Минусы:
        • Функциональность: Меньше функций по сравнению с Kubernetes.
        • Экосистема: Меньше дополнительных инструментов и сообщество не такое большое.

    • Вопрос №7: [ OpenShift, плюсы и минусы? ]

      Ответ
      • Плюсы:
        • Безопасность: Встроенные функции безопасности, такие как секреты и политики доступа.
        • Полнота решения: Предлагает комплексное решение для CI/CD, мониторинга и логирования.
        • Поддержка: Поддерживается Red Hat, что обеспечивает хорошую корпоративную поддержку.
      • Минусы:
        • Сложность: Может быть сложным для новичков.
        • Цена: Бесплатная версия ограничена, полные версии могут быть дорогими.

    • Вопрос №8: [ Mesos, плюсы и минусы? ]

      Ответ
      • Плюсы:
        • Масштабируемость: Разработан для очень больших кластеров и больших объемов данных.
        • Гибкость: Поддерживает множество типов рабочих нагрузок, включая не только контейнеры.
        • Высокая производительность: Оптимизирован для высокой производительности и эффективности.
      • Минусы:
        • Сложность: Сложная архитектура и высокий порог входа.
        • Экосистема: Меньше готовых решений и интеграций по сравнению с Kubernetes.

  • CI/CD (Непрерывная интеграция и непрерывная доставка)
    • Вопрос №1: [ Что такое такое CI/CD? ]

      Ответ
      • CI/CD — это методология разработки программного обеспечения, которая использует автоматизацию для ускорения процессов сборки, тестирования и развертывания приложений. Эти аббревиатуры расшифровываются следующим образом:\

        • CI (Continuous Integration, непрерывная интеграция): Этот процесс включает в себя автоматическую сборку и тестирование кода. Каждый раз, когда разработчики вносят изменения в код (обычно это делается путем слияния изменений в основную ветку репозитория), автоматизированные системы собирают приложение и запускают набор тестов для удостоверения, что изменения не внесли новых ошибок. Цель — быстро и эффективно интегрировать новый код с существующим кодовой базой.\

        • CD (Continuous Deployment / Continuous Delivery, непрерывное развертывание / непрерывная поставка): Эти термины часто используются взаимозаменяемо, хотя они обозначают немного разные концепции.
          Continuous Delivery подразумевает, что каждый успешно протестированный код автоматически подготавливается к развертыванию в продакшене. Однако само развертывание обычно требует одобрения человека.
          Continuous Deployment идет шаг дальше и автоматически развертывает каждую успешно протестированную версию кода в продакшен без необходимости вмешательства человека.\

        • Преимущества CI/CD: Быстрота релизов: Автоматизация процессов сборки, тестирования и развертывания значительно ускоряет процесс выпуска новых версий продукта.
          Повышение качества продукта: Благодаря автоматическим тестам и другим проверкам шансы на появление багов или ошибок в продакшене снижаются.
          Эффективность и снижение затрат: Процесс разработки становится более предсказуемым и менее трудоемким, что снижает общие затраты на разработку и поддержку продукта.
          Легкость внедрения изменений: С возможностью быстрого и надежного развертывания нового кода, команды могут чаще и с меньшими рисками внедрять изменения, включая новые функции, исправления ошибок и улучшения производительности.
          Сотрудничество и масштабируемость: CI/CD облегчает сотрудничество между командами и делает процесс разработки более масштабируемым.
          Откат изменений: В случае проблем с новой версией продукта, возможность быстрого отката изменений до предыдущей рабочей версии является важным преимуществом.\


    • Вопрос №2: [ Какие системы CI/CD знаешь? ]

      Ответ
      • Jenkins
      • GitLab CI/CD
      • Travis CI
      • CircleCI
      • Spinnaker
      • TeamCity

    • Вопрос №3: [ Какие лучшие практики CI/CD вы знаете? Или что вы считаете лучшей практикой CI/CD? ]

      Ответ
      • Чаще фиксируйте и тестируйте.
      • Среда тестирования/промежуточного тестирования должна быть клоном производственной среды.
      • Очистите свою среду (например, ваши конвейеры CI/CD могут создавать много ресурсов. Они также должны позаботиться об очистке всего, что они создают).
      • Конвейеры CI/CD должны обеспечивать одинаковые результаты при локальном или удаленном выполнении.
      • Относитесь к CI/CD как к еще одному приложению в вашей организации. Не как связующий код.
      • Среды по требованию вместо заранее выделенных ресурсов для целей CI/CD
      • Этапы/шаги/задачи конвейеров должны распределяться между приложениями и микросервисами (не изобретайте заново общие задачи, такие как «клонирование проекта»).

    • Вопрос №4: [ Где вы храните конвейеры CI/CD? Почему?? ]

      Ответ
      • Существует несколько подходов к тому, где хранить определения конвейера CI/CD:
        • Репозиторий приложений — храните их в том же репозитории приложения, которое они создают или тестируют (возможно, самого популярного).
        • Центральный репозиторий — храните все конвейеры CI/CD организации/проекта в одном отдельном репозитории (возможно, лучший подход, когда несколько команд тестируют один и тот же набор проектов, и в конечном итоге у них получается много конвейеров).
        • Репозиторий CI для каждого репозитория приложения — вы отделяете код, связанный с CI, от кода приложения, но не помещаете все в одно место (возможно, худший вариант из-за обслуживания)
        • Платформа, на которой выполняются конвейеры CI/CD (например, кластер Kubernetes в случае конвейеров Tekton/OpenShift).

    • Вопрос №5: [ Как вы планируете плановую мощность для ресурсов CI/CD? (например, серверы, хранилище и т. д.)? ]

      Ответ
      • Планирование мощности ресурсов CI/CD включает оценку ресурсов, необходимых для поддержки конвейера CI/CD, и обеспечение достаточной мощности инфраструктуры для удовлетворения потребностей конвейера. Вот несколько шагов по планированию мощности для ресурсов CI/CD:
        • Анализируйте рабочую нагрузку
        • Мониторинг текущего использования
        • Выявление узких мест в ресурсах
        • Прогноз будущего спроса
        • План роста
        • Учитывайте масштабируемость и эластичность
        • Оценить стоимость и бюджет
        • Постоянно контролировать и корректировать\
      • Выполнив эти шаги, вы сможете эффективно планировать емкость ресурсов CI/CD, гарантируя, что ваш конвейер имеет достаточные ресурсы для эффективной работы и удовлетворения потребностей вашего процесса разработки.

  • Мониторинг и логирование
    • Вопрос №1: [ Что такое системы мониторинга и логирования? ]

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

      • Системы мониторинга Системы мониторинга предназначены для непрерывного отслеживания состояния серверов, приложений, сетевого оборудования и других системных ресурсов. Они собирают данные с помощью метрик, таких как использование CPU, объем занятой памяти, загрузка сети и др. Ключевые характеристики:

        • Реальное время: Системы мониторинга работают в реальном времени, предоставляя актуальные данные.
        • Алерты: Они могут автоматически отправлять уведомления при обнаружении аномалий или проблем.
        • Визуализация: Часто включают графики и дашборды для наглядного представления данных.
      • Системы логирования Системы логирования собирают и хранят логи — текстовые или бинарные файлы, которые содержат информацию о событиях, произошедших в операционной системе или приложении. Ключевые характеристики:

        • Хранение данных: Логи обычно хранятся в файлах или специализированных хранилищах.
        • Анализ: Системы логирования могут анализировать эти данные для выявления шаблонов, проблем или аномалий.
        • Поиск и фильтрация: Предоставляют мощные инструменты для поиска и фильтрации данных.
      • Совместное использование Обе эти системы часто используются вместе для обеспечения полного контроля над инфраструктурой и приложениями:

        Мониторинг может быть настроен для отслеживания событий, зарегистрированных системами логирования. Логи могут быть анализированы для диагностики проблем, выявленных системами мониторинга. В современных DevOps-практиках использование этих систем является стандартом для эффективного управления инфраструктурой и обеспечения высокой доступности и производительности


    • Вопрос №2: [ Какие системы мониторинга, логирования и трассировки знаешь? ]

      Ответ
      • Трассировка: Jaeger, Zipkin.
      • Мониторинг: Prometheus, Grafana, Zabbix.
      • Логирование: ELK Stack (Elasticsearch, Logstash, Kibana), Grafana Loki.

  • Облачные сервисы
    • Вопрос №1: [ Что такое облачный сервис? ]

      Ответ
      • Облачный сервис — это услуга, предоставляемая посредством интернета. Эти услуги часто предоставляются на основе "по требованию" и позволяют пользователям получить доступ к различным ресурсам и приложениям без необходимости покупки и установки специализированного программного обеспечения или аппаратных средств. Облачные сервисы обычно хостятся в центрах обработки данных, управляемых поставщиками этих услуг.

        Облачные сервисы можно разделить на несколько основных категорий:

      • Инфраструктура как услуга (IaaS) Это базовая форма облачных сервисов, которая предоставляет виртуализованные вычислительные ресурсы через интернет.\ Примеры включают Amazon Web Services (AWS), Microsoft Azure и Google Cloud Platform (GCP).

      • Платформа как услуга (PaaS) Предоставляет платформу и окружение, позволяющие разработчикам создавать, развертывать и управлять приложениями без забот об аппаратном обеспечении и программном обеспечении.
        Примеры включают Heroku, Microsoft Azure App Services и Google App Engine.

      • Программное обеспечение как услуга (SaaS) Это программное обеспечение, доступ к которому предоставляется через интернет. Эти приложения обычно доступны через веб-браузер.
        Примеры включают Google Workspace (ранее G Suite), Microsoft Office 365 и Salesforce.

      • Функция как услуга (FaaS) или Serverless Это вычислительные сервисы, которые автоматически масштабируются в зависимости от потребностей и позволяют пользователям запускать код в ответ на определенные события.
        Примеры включают AWS Lambda и Azure Functions.

      • Облачное хранилище Предоставляет место для хранения данных в облаке, которое можно легко масштабировать и доступ к которому можно получить из любого места.
        Примеры включают Google Drive, Dropbox и Amazon S3.

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


    • Вопрос №2: [ Какие облачные сервисы знаешь? ]

      Ответ
      • Amazon Web Services (AWS)
      • Google Cloud Platform (GCP)
      • Microsoft Azure
      • IBM Cloud

  • Среды виртуализации и управление облачной инфраструктурой
    • Вопрос №1: [ Что такое среда виртуализации и управления облачной инфраструктурой? ]

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

    • Вопрос №2: [ Какие среда виртуализации и управления облачной инфраструктурой знаешь? ]

      Ответ
      • Terraform
      • Vagrant
      • VMware

  • Сетевые и безопасные инструменты
    • Вопрос №1: [ Что такое сетевые и безопасные инструменты? ]

      Ответ
      • Сетевые и безопасные инструменты предназначены для управления, мониторинга и защиты сетевой инфраструктуры и данных. Они охватывают широкий спектр функций, включая, но не ограничиваясь, следующими:
      • Сетевые инструменты
        • Маршрутизаторы и коммутаторы: Оборудование для маршрутизации и коммутации сетевого трафика.
        • Сетевые анализаторы и мониторы: Программы для анализа сетевого трафика, такие как Wireshark.
        • Балансировщики нагрузки: Устройства или программы, распределяющие входящий трафик между несколькими серверами.
        • VPN (Virtual Private Network): Инструменты для создания зашифрованных сетевых туннелей между устройствами в разных географических точках. DNS серверы: Службы для преобразования доменных имен в IP-адреса.
        • Системы управления сетью (NMS): Платформы для мониторинга и управления сетевыми устройствами, такие как Zabbix и SolarWinds.
      • Инструменты безопасности
        • Брандмауэры: Аппаратные или программные решения для фильтрации сетевого трафика и блокировки нежелательных подключений.
        • Системы предотвращения вторжений (IPS) и системы обнаружения вторжений (IDS): Решения для мониторинга и анализа сетевого трафика на предмет аномалий и атак.
        • Антивирусные программы: Программы для сканирования и удаления вредоносного программного обеспечения.
        • Сканеры уязвимостей: Инструменты для автоматического обнаружения уязвимостей в системах и приложениях.
        • Инструменты для управления доступом и идентификации (IAM): Решения для управления учетными записями пользователей и их правами доступа.
        • Шифрование данных: Инструменты для шифрования данных в покое или в передаче.
      • Сетевые и безопасные инструменты играют критически важную роль в современных IT-системах. Они обеспечивают стабильность, производительность и безопасность, предотвращая несанкционированный доступ и сбои в системе.

    • Вопрос №2: [ Какие знаешь сетевые и безопасные инструменты? ]

      Ответ
      • NGINX
      • Apache HTTP Server
      • iptables
      • HAProxy


Принципы и методологии
  • Вопрос №1: [ Что такое SOLID? ]

    Ответ
    • SOLID — это аббревиатура, которая представляет пять основных принципов объектно-ориентированного программирования и проектирования:
    • S - Single Responsibility Principle (Принцип единственной ответственности): Класс должен иметь только одну причину для изменения. В других словах, у него должна быть только одна задача или ответственность.
    • O - Open/Closed Principle (Принцип открытости/закрытости): Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации.
    • L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков): Объекты в программе должны быть заменяемыми на экземпляры их подтипов, без изменения желательности программы.
    • I - Interface Segregation Principle (Принцип разделения интерфейса): Клиенты не должны быть вынуждены зависеть от интерфейсов, которые они не используют.
    • D - Dependency Inversion Principle (Принцип инверсии зависимостей): Зависимость на Абстракциях. Нет зависимости на что-то конкретное.

  • Вопрос №2: [ Что такое KISS? ]

    Ответ
    • KISS (Keep It Simple, Stupid) — это принцип проектирования, который утверждает, что большинство систем работают лучше всего, если они остаются простыми, а не усложненными. Сложность должна быть устранена там, где это возможно.

  • Вопрос №3: [ Что такое DRY? ]

    Ответ
    • DRY (Don’t Repeat Yourself) — это принцип разработки программного обеспечения, нацеленный на уменьшение повторения информации. Любая логика или определение должны существовать в единственном месте в коде, что уменьшает вероятность ошибок и упрощает поддержку.

  • Вопрос №4: [ Что такое YAGNI? ]

    Ответ
    • YAGNI (You Aren't Gonna Need It) — это принцип, который гласит, что программисты не должны добавлять функциональность до тех пор, пока это не станет абсолютно необходимым. Это напоминает о том, что добавление ненужных функций может привести к дополнительным затратам времени и ресурсов, не принося при этом реальной пользы.

  • Вопрос №5: [ Что такое инъекция зависимости (Dependency Injection)? ]

    Ответ
    • Инъекция зависимостей (Dependency Injection, DI) — это метод проектирования программного обеспечения, который упрощает управление зависимостями между объектами. Этот принцип может быть применен в любом языке программирования, включая Go (Golang). В Go для инъекции зависимостей часто используются интерфейсы и структуры.

    • Это упрощает тестирование (можно использовать мок-объекты) и повышает гибкость кода, делая его менее зависимым от конкретных реализаций. В Go встроенных фреймворков для DI нет, но язык предоставляет все необходимые средства для реализации этого принципа самостоятельно или с помощью сторонних библиотек.


GIT
  • Вопрос №1: [ Что такое GIT? ]

    Ответ
    • Git — это система управления версиями, которая позволяет нескольким разработчикам совместно работать над одним проектом. Каждый разработчик может клонировать репозиторий, работать над кодом локально, делать коммиты (фиксировать изменения), и затем загружать эти изменения обратно в общий репозиторий.

  • Вопрос №2: [ Как клонировать репозиторий с помощью Git? ]

    Ответ
    • Для клонирования репозитория используется команда git clone [URL репозитория]

  • Вопрос №3: [ Что делает команда git add -A? ]

    Ответ
    • Команда git add -A добавляет все изменения в файлах (новые, измененные, удаленные) в индекс для следующего коммита.

  • Вопрос №4: [ Как посмотреть историю коммитов? ]

    Ответ
    • Используйте git log для просмотра истории коммитов. С флагом --oneline, история будет отображена в сжатом однострочном формате.

  • Вопрос №5: [ Что делает команда git commit -m "Сообщение"? ]

    Ответ
    • Команда git commit -m "Сообщение" создает новый коммит с заданным сообщением, описывающим изменения.

  • Вопрос №6: [ Как переключиться на другую ветку? ]

    Ответ
    • Команда git checkout [имя_ветки] позволяет переключиться на указанную ветку.
    • Команда git switch [имя_ветки] позволяет переключиться на указанную ветку.

  • Вопрос №7: [ Как создать новую ветку и переключиться на нее? ]

    Ответ
    • Команда git checkout -b [имя_новой_ветки] создает новую ветку и сразу переключает на нее рабочий каталог.

  • Вопрос №8: [ Как удалить ветку локально? ]

    Ответ
    • Для удаления локальной ветки используется git branch -d [имя_ветки] или git branch -D [имя_ветки] для принудительного удаления.

  • Вопрос №9: [ Как обновить локальный репозиторий, синхронизировав его с удаленным? ]

    Ответ
    • Использовать git pull [имя_удаленного] [имя_ветки] для получения изменений из удаленного репозитория и их интеграции с вашим локальным репозиторием.

  • Вопрос №10: [ Как посмотреть состояние рабочего каталога и индекса? ]

    Ответ
    • Команда git status покажет состояние файлов в рабочем каталоге и индексе: какие файлы изменены, но не добавлены в индекс; какие ожидают коммита.

  • Вопрос №11: [ Что делает команда git reset --hard HEAD~1? ]

    Ответ
    • Эта команда откатывает репозиторий на один коммит назад, удаляя последний коммит. Она опасна, потому что утрачивает изменения.

  • Вопрос №12: [ Что делает команда git push -f или git push --force? ]

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

  • Вопрос №13: [ Что делает команда git clean -fd? ]

    Ответ
    • Эта команда удалит неотслеживаемые файлы и директории. Опасна, потому что может удалить важные файлы, которые еще не были добавлены в репозиторий.

2️⃣ Языки программирования:

Golang
  • Общие вопросы по языку Go
    • Вопрос №1: [ Расскажи кратко о языке Go ]

      Ответ
      • Go (Golang) — это компилируемый многопоточный язык программирования от Google с открытым исходным кодом. Считается языком общего назначения, но основное применение — разработка веб-сервисов и клиент-серверных приложений.
        • Язык Go был представлен в 2009 году в корпорации Google. Его полное название — Golang — производное от «Google language». Язык создали Роб Пайк и Кен Томпсон.
      • У языка: Строгая статическая типизация, понятный и простой синтаксис, встроеный «сборщика мусора»

    • Вопрос №2: [ Как реализовано хранилище памяти в Go? ]

      Ответ
      • Хранилища памяти в Go реализованы с помощью двух подходов:
      • Хранение в stack. в основном используется для хранения локальных переменных, аргументов функции. Из плюсов -stack достаточно легко очищается. Из минусов - при аллокациях на stack существуют копии одних и тех же значений, которые надо хранить и обрабатывать.
      • Хранение в heap. в основном используется для хранения глобальный переменных и ссылочных типов. Из плюсов - при аллокациях на heap существует всегда одно уникальное значение, которое надо хранить и обрабатывать. Из минусов - heap тяжело очищается, так как приходится запускать сборщик мусора, который имеет много накладных расходов и останавливает приложение.

    • Вопрос №3: [ Какие типы данных есть в языке Go? ]

      Ответ
      • Boolean: bool (значения true или false)

      • Целочисленные типы: int и uint: знаковые и беззнаковые целые числа, размер зависит от платформы (32 или 64 бита) int8, int16, int32, int64: знаковые целые числа с фиксированным размером uint8, uint16, uint32, uint64: беззнаковые целые числа с фиксированным размером uintptr: беззнаковый целочисленный тип, достаточный для хранения разыменованного указателя

      • Числа с плавающей точкой: float32, float64: числа с плавающей точкой

      • Комплексные числа: complex64, complex128: комплексные числа

      • Строки и символы: Строки: string Байты: byte (эквивалент типа uint8)

      • Составные типы: Массивы: например, [5]int (массив из 5 целых чисел) Срезы: например, []int (динамически изменяемый массив) Map (ассоциативный массив): например, map[string]int Структуры: например, struct { Name string; Age int }

      • Другие типы: Интерфейсы: interface{} Каналы: chan Указатели: например, *int (указатель на целое число)


    • Вопрос №4: [ Что такое пакеты в go? ]

      Ответ
      • Пакет - это механизм переиспользования кода, при котором go файлы помещаются в общую директорию. В начале каждого такого файла объявляется зарезервированное слово package, а после него прописывается имя пакета. В рамках пакета все функции и глобальные переменные, экспортируемые(начинаются с заглавной буквы) и не экспортируемые(начинаются со строчной буквы), видят друг друга.

    • Вопрос №5: [ Что такое глобальная переменная? ]

      Ответ
      • Глобальная переменная - это переменная уровня пакета, то есть объявленная вне функции. Глобальная переменная также может быть доступна за рамками пакета, конечно только в том случае, если ее наименование начинается в верхнем регистре.

    • Вопрос №6: [ Что такое константы и можно ли их изменять? ]

      Ответ
      • Константы - это неизменяемые переменные, изменить константу нельзя.

    • Вопрос №7: [ Зачем фигурные скобки с не объявленным оператором внутри функции? ]

      Ответ
      • В go функции действительно можно объявить {} без оператора, ограничив область видимости куска кода в рамках этой функции.

    • Вопрос №8: [ В go есть оператор switch case, можно ли выполнить несколько условий в одном объявленном операторе? ]

      Ответ
      • Такое возможно благодаря ключевому слову fallthrough. Оно заставляет выполнять код в следующей объявленной булевой секции, вне зависимости подходит ли булевое условие case этой секции.

    • Вопрос №9: [ Что такое iota? ]

      Ответ
      • iota - это идентификатор, который позволяет создавать последовательные не типизированные целочисленные константы. Значением iota является индекс ConstSpec. Несмотря на то, что первым индексом является 0, значение первой константы можно задать отличным от 0, что в свою очередь повлияет на значения последующих констант.

    • Вопрос №10: [ Как вручную задать количество процессоров для приложения? ]

      Ответ
      • Это позволяет сделать runtime.GOMAXPROCS(). Важно понимать, что при выставлении количества логических процессоров больше, чем есть у вас в системе, вы рискуете получить определенные проблемы с производительностью. Чтобы избежать этого можно задать runtime.GOMAXPROCS(runtime.NumCPU()), runtime.NumCPU() - количество логических процессоров.

    • Вопрос №11: [ Как принудительно переключить контекст? ]

      Ответ
      • Переключение контекста вручную осуществляется с помощью функции runtime.Goshed().

    • Вопрос №12: [ Что такое graceful shutdown? ]

      Ответ
      • У каждого сервера есть потребность в его отключении, обычно это происходит при получении сигнала от ОС. И хорошо бы делать это отключение корректно, останавливая поэтапно все службы. Согласитесь никто из нас не выключает телевизор ударом табурета по корпусу. Так же и с сервером, для корректного отключения которого есть общие подходы. К примеру:
      • создать канал, прослушивающий системные сигналы на выход;
      • прослушивать этот канал;
      • при получении сигнала поэтапно выходить из горутин;
      • остановить сервер.

    • Вопрос №13: [ Что обозначает * и &? ]

      Ответ
      • "&" - это адрес блока памяти. То есть &myVar - это адрес того места в памяти, где хранятся данные переменной myVar. Тогда как "*" можно использовать в двух вариантах: чтобы объявить тип-указатель var pointVar *int. В данном случае указатель на int; чтобы получить значение по адресу *pointVar. Обратный предыдущему процесс, и здесь мы получим значение по адресу pointVar.

    • Вопрос №14: [ Как происходит передача параметров в функцию? ]

      Ответ
      • Параметры в Go всегда передаются по значению. Это значит, что всякий раз, когда мы передаем аргумент в функцию, функция получает копию первоначального значения. Чтобы работать именно с той же самой переменной, не копируя ее, необходимо использовать адрес этой переменной. При этом сам указатель будет скопирован.

    • Вопрос №15: [ Есть ли особенности поведения при передаче map и slice в функцию? ]

      Ответ
      • Передача slice и map может заставить усомниться в том, что они передаются в функцию по значению. Однако здесь так же происходит копирование. Структуры slice и map (уточнение: в случае map копируется не сама структура, а указатель на структуру hmap, подробнее о том, что такое hmap можно прочитать в документации) копируются, однако в самих структурах содержатся ссылки на области памяти, благодаря которым создается эффект передачи по ссылке.

    • Вопрос №16: [ Как функции делятся памятью? ]

      Ответ
      • В начале следует сказать про фрейм. Фрейм можно представить как отдельное пространство памяти для конкретной функции. Функция может работать с памятью в своем фрейме, однако не может работать с памятью фреймов других функций. Когда из одной функции мы вызываем другую функцию, происходит переход фреймов. Чтобы использовать какие-то данные предыдущего фрейма в следующем их можно передать по значению. Если необходимо работать не с копией, а именно переменной другого фрейма, необходимо использовать переменные-указатели, которые обеспечивают доступ до переменных других фреймов.

    • Вопрос №17: [ Опиши роль среды исполнения в Go (runtime, за что он отвечает?) ]

      Ответ
      • Среда исполнения (runtime) в Go отвечает за множество низкоуровневых задач, которые обеспечивают высокоуровневые функциональные возможности языка. Давайте подробно рассмотрим каждый из перечисленных аспектов:
        • Менеджмент потоков (Thread Management) Go runtime автоматически создаёт и управляет системными потоками, на которых будут запущены горутины. Он может автоматически увеличивать или уменьшать количество потоков в зависимости от нагрузки.
        • Шедулинг горутин (Goroutine Scheduling) Горутины — это легковесные потоки, управляемые средой исполнения Go. Go runtime содержит планировщик (scheduler), который решает, какие горутины должны быть активированы и на каких системных потоках. Планировщик работает на уровне языка, а не на уровне операционной системы, что делает его более эффективным и легковесным.
        • Сетевые вызовы, netpoller Go runtime включает в себя механизм netpoller для асинхронной работы с сетевыми соединениями. Это позволяет эффективно масштабировать сетевые приложения, так как не требуется создавать новый поток для каждого входящего соединения.
        • Менеджмент памяти (Memory Management) Go runtime отвечает за выделение и освобождение памяти, создание и удаление объектов и так далее. Это обеспечивает автоматическую управляемость памятью и помогает разработчикам избежать ошибок, связанных с её управлением.
        • Сборка мусора (Garbage Collection) Go использует автоматическую сборку мусора для удаления неиспользуемых объектов из памяти. Это уменьшает вероятность утечек памяти и других ошибок, связанных с управлением ресурсами.
        • Profiling и Race Detector Среда исполнения Go включает в себя инструменты для профилирования (profiling) и обнаружения состояний гонки (race conditions). Эти инструменты полезны для оптимизации производительности и обеспечения качества кода.

    • Вопрос №18: [ Что такое "data race" и "race condition"? Это одно и то же? ]

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

      • Data Race (Гонка данных): Что это: Data race возникает, когда две или более операции выполняются параллельно, и хотя бы одна из них является записью, и операции ссылаются на одну и ту же переменную в памяти. Следствия: Непредсказуемое или неожиданное значение переменной. Решение: Использование мьютексов, каналов или других средств синхронизации для обеспечения исключительного доступа к данным. Пример: Два потока пытаются одновременно изменить значение одной и той же переменной без использования мьютекса.

      • Race Condition (Условие гонки): Что это: Более общий термин, означающий ситуацию, в которой поведение системы зависит от относительного порядка или времени событий, таких как доступ к ресурсам, операции I/O и так далее. Следствия: Непредсказуемое или некорректное поведение программы или системы. Решение: Сложнее выявить и исправить, часто требует дизайнерских изменений, включая введение механизмов синхронизации или очередей. Пример: Два потока пытаются одновременно считать и обновить значение в базе данных, что может привести к неконсистентному или некорректному состоянию данных.

      • В общем, все data races являются race conditions, но не все race conditions являются data races. Data race — это частный случай условия гонки, когда есть конкурентный доступ к памяти. Условие гонки может возникнуть в более широком контексте и может включать в себя любые типы системных ресурсов, не только переменные в памяти


    • Вопрос №19: [ Что такое type switch? ]

      Ответ
      • В Go, конструкция type switch используется для определения типа переменной в интерфейсе. Это полезно, когда у вас есть переменная интерфейсного типа и вы хотите выполнить разные действия в зависимости от её конкретного типа. Type switch в Go работает аналогично обычному switch, но вместо значений переменной проверяются её типы.

        Пример:

        func main() {
          var x interface{}
          x = "Hello"
        
          // Использование type switch для определения типа x
          switch v := x.(type) {
          case int:
              fmt.Println("Это целое число:", v)
          case float64:
              fmt.Println("Это число с плавающей точкой:", v)
          case string:
              fmt.Println("Это строка:", v)
          default:
              fmt.Println("Неизвестный тип")
          }
        }

    • Вопрос №20: [ Как происходит переключение контекста в golang? ]

      Ответ
      • В Go переключение контекста прежде всего связано с горутинами и планировщиком Go.
      • Горутины:
        В Go, конкурентное выполнение кода достигается с помощью горутин, которые представляют собой легковесные потоки выполнения. Они многократно легче традиционных потоков ОС, и можно иметь тысячи или даже миллионы горутин, выполняющихся в рамках одного процесса.
      • Планировщик Go: Планировщик в Go обрабатывает все горутины, выполняющиеся в программе. Он отвечает за старт, остановку и переключение между горутинами. Планировщик Go работает в рамках потока ОС и управляет множеством горутин в этом потоке.
      • Как происходит переключение:
        • Кооперативная многозадачность: Горутины выполняются кооперативно. Это означает, что горутина будет продолжать работать до тех пор, пока она не достигнет "точки сдачи" (yield point), в которой она может передать управление обратно планировщику. Примеры таких точек: системные вызовы, операции с каналами, функции sleep и другие.
        • Preemption (прерывание): В более старых версиях Go, планировщик прерывал горутины на основе кооперативной многозадачности. Однако начиная с Go 1.14, был добавлен механизм прерывания, который позволяет планировщику прерывать долго работающие горутины, даже если они не достигли "точки сдачи".
      • GOMAXPROCS: Это значение определяет, сколько потоков ОС может использоваться планировщиком для параллельного выполнения горутин. По умолчанию, это равно количеству ядер на машине. Если GOMAXPROCS равно 1, все горутины будут выполняться в одном потоке ОС, и в этом случае переключение контекста произойдет только тогда, когда текущая горутина достигает "точки сдачи".
      • Переключение между потоками ОС и горутинами: Хотя горутины легче и эффективнее, чем потоки ОС, иногда может потребоваться переключение на другой поток ОС (например, при блокирующем системном вызове). Планировщик Go интеллектуально управляет этим, минимизируя накладные расходы на переключение контекста.
      • В общем, переключение контекста в Go разработано для максимизации производительности и позволяет эффективно управлять большим количеством горутин с минимальными накладными расходами.

  • Численные типы
    • Вопрос №1: [ Какие численные типы есть? ]

      Ответ
      • Целочисленные типы:
        int8: 8-битное знаковое целое число (-128 до 127)
        int16: 16-битное знаковое целое число (-32,768 до 32,767)
        int32 (rune): 32-битное знаковое целое число (-2,147,483,648 до 2,147,483,647)
        int64: 64-битное знаковое целое число (-9,223,372,036,854,775,808 до 9,223,372,036,854,775,807)
        uint8 (byte): 8-битное беззнаковое целое число (0 до 255)
        uint16: 16-битное беззнаковое целое число (0 до 65,535)
        uint32: 32-битное беззнаковое целое число (0 до 4,294,967,295)
        uint64: 64-битное беззнаковое целое число (0 до 18,446,744,073,709,551,615)
        int: знаковое целое число, размер зависит от платформы (обычно 32 или 64 бита)
        uint: беззнаковое целое число, размер зависит от платформы (обычно 32 или 64 бита)
        uintptr: беззнаковое целое число, достаточное для хранения разыменованного указателя (размер зависит от платформы)

      • Числа с плавающей точкой:
        float32: 32-битное число с плавающей точкой (приблизительный диапазон от 1.4E-45 до 3.4E+38)
        float64: 64-битное число с плавающей точкой (приблизительный диапазон от 4.9E-324 до 1.8E+308)

      • Комплексные числа:
        complex64: комплексное число с двумя 32-битными числами с плавающей точкой (для действительной и мнимой частей)
        complex128: комплексное число с двумя 64-битными числами с плавающей точкой (для действительной и мнимой частей)


    • Вопрос №2: [ Какой результат получим если разделить int на 0 и float на 0? ]

      Ответ
      • Это вопрос с подвохом. Деление int на 0 в go невозможно и вызовет ошибку компилятора. Тогда как деление float на 0 дает в своем результате бесконечность.

  • Строки
    • Вопрос №1: [ Что представляют собой строки в go? ]

      Ответ
      • Строки в go - это обычный массив байт. Это надо понимать для того, чтобы ответить на следующие вопросы о строках.

      • Как можно оперировать строками? Строки в go можно складывать(конкатенировать), сравнивать, получить срез, длинну, и т.д

      • Что будет если сложить строки? Мы будем получать новые строки

      • Как определить количество символов для строки?" или "Какие есть нюансы при итерации по строке? Исходя из того же знания, что строка это массив байт, взяв базовую функцию len() от строки мы получим количество байт. Похожее поведение будет при итерации по строке - итерация по байтам. Тогда как в зависимости от кодировки, символ в строке может занимать не один байт. Для того, чтобы работать именно с символами, необходимо преобразовать строку в тип []rune. Еще одним способом определения длинны строки является функция RuneCountInString пакета utf8.


    • Вопрос №2: [ Как преобразовать строку в int и наоборот? Можно ли сделать int(string) и string(int) соответственно? ]

      Ответ
      • Преобразование типов между int и string указанным синтаксисом невозможно. Для преобразования необходимо использовать функции из пакета strconv стандартной библиотеки go. При этом для преобразования строк в/из int и int64 используются разные функции, strconv.Atoi и strconv.Itoa для int, strconv.ParseInt и strconv.FormatInt соответственно.

  • Интерфейсы
    • Вопрос №1: [ Интерфейсы: Что такое интерфейс в Go? Зачем нужен на практике? Примеры задач где стоит ввести? ]

      Ответ
      • В Go, интерфейс — это набор сигнатур методов (контракт). Тип, реализующий все методы, указанные в интерфейсе, считается реализующим этот интерфейс. Особенностью языка Go является неявная реализация интерфейсов: вам не нужно явно указывать, что тип реализует интерфейс.

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

      • Примеры задач, где стоит ввести интерфейс Логирование: Если у вас есть несколько способов логирования (в файл, в БД, через сеть), вы можете определить интерфейс Logger с методом Log, и затем реализовать его различными способами. Сетевые запросы: Если ваше приложение взаимодействует с различными внешними API, вы можете создать интерфейс APIClient с методами, которые нужны для взаимодействия с API. Тестирование: Интерфейсы позволяют легко мокать зависимости, что упрощает тестирование.


    • Вопрос №2: [ Что такое пустой интерфейс? ]

      Ответ
      • В Go, пустой интерфейс interface{} не имеет методов. Это означает, что любой тип автоматически реализует пустой интерфейс, и вы можете присвоить значение любого типа переменной пустого интерфейса. Это обычно используется для создания контейнеров, которые могут хранить значения любого типа, или для функций, которые могут принимать аргументы любого типа.

    • Вопрос №3: [ Как устроен внутри nil интерфейс vs nil внутри интерфейса? ]

      Ответ
      • Под капотом, интерфейс в Go — это двухсловная структура, содержащая: Type: Указатель на информацию о типе. Это позволяет интерфейсу знать, какой именно тип он хранит. Data: Указатель на само значение. Для пустого интерфейса эта структура особенно полезна, потому что Type будет указывать на реальный тип данных, хранящихся в Data, что позволяет динамически определять тип при выполнении (runtime). Этот механизм делает интерфейсы относительно медленными по сравнению с конкретными типами, так как добавляет дополнительный уровень индирекции и необходимость динамического определения типов. Однако это не всегда критично и является приемлемой "ценой" за удобство и гибкость интерфейсов. Таким образом, использование пустого интерфейса в Go — это удобный, но не всегда оптимальный с точки зрения производительности способ работы с данными неизвестного или переменного типа.

           var a interface{} 
           var b *int 
           a=b 
           fmt.Println("ab", a==nil)
        • Nil интерфейс
          Когда мы говорим, что интерфейс равен nil, это означает, что оба поля внутренней структуры интерфейса (Type и Data) равны nil. Это можно представить как "абсолютный" nil для интерфейса.
           var a interface{}
           fmt.Println(a == nil)  // Вывод: true
        • Nil внутри интерфейса
          Пример var b *int создает указатель на int, который равен nil. Однако, когда этот nil указатель присваивается интерфейсной переменной a, поле Type внутренней структуры интерфейса теперь указывает на тип *int, в то время как поле Data равно nil.
           var a interface{}
           var b *int
           a = b
           fmt.Println(a == nil)  // Вывод: false
        • Здесь a == nil вернет false, потому что, хотя Data равно nil, Type указывает на тип *int. С точки зрения интерфейса, это не nil. Этот аспект может иногда приводить к неожиданному поведению и ошибкам, и его важно понимать при работе с интерфейсами в Go.

    • Вопрос №4: [ Как определить тип интерфейса? ]

      Ответ
      • С помощью инструкции switch case и приведения типа можно определить тип интерфейса, указав возможные варианты базового типа его значения.
         switch v := animal.(type) {
         case Dog:
         fmt.Println("It's a dog:", v.Speak())
         default:
         fmt.Println("Unknown type")
         }

    • Вопрос №5: [ В каком пакете лучше объявлять интерфейсы и почему? ]

      Ответ
      • В Go интерфейсы часто объявляются в том пакете, который будет использовать, а не реализовывать, этот интерфейс. Это принципиально отличается от некоторых других языков программирования, где интерфейсы часто объявляются в том же пакете, что и их реализации. Рассмотрим причины этого:

        Цель интерфейса Интерфейс в Go — это определение "контракта": он описывает, что должен делать тип, но не как. Клиентский код, который опирается на этот "контракт", важнее, чем реализации, потому что интерфейс обеспечивает абстракцию, которая позволяет клиентскому коду не зависеть от конкретных реализаций.

        Разделение зависимостей Если вы помещаете интерфейс в пакет, который будет его использовать, то этот пакет не становится зависимым от всех пакетов, которые реализуют этот интерфейс. Это упрощает управление зависимостями.

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

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


    • Вопрос №6: [ Как сообщить компилятору, что наш тип реализует интерфейс? ]

      Ответ
      • В Go, вы не должны явно указывать, что определённый тип реализует интерфейс. Вместо этого, компилятор автоматически определяет это, проверяя наличие всех необходимых методов в вашем типе. Это называется "неявной реализацией интерфейсов"

  • Массивы и слайсы
    • Вопрос №1: [ Что такое слайс и чем он отличается от массива? ]

      Ответ
      • Cлайс - это структура go, которая включает в себя ссылку на базовый массив, а также две переменные len(length) и cap(capacity). len это длина слайса - то количество элементов, которое в нём сейчас находится. cap - это ёмкость слайса - то количество элементов, которые мы можем записать в слайс сверх len без его дальнейшего расширения. Array - это последовательно выделенная область памяти. Частью типа array является его размер, который в том числе является не изменяемым.

    • Вопрос №2: [ Какой размер массива выделяется под слайс при его расширении за рамки его емкости? ]

      Ответ
      • Если отвечать на вопрос поверхностно, то можно сказать, что базовый массив расширяется в два раза от нашей capacity. Отвечая более емко, следует учесть, что при больших значениях расширение будет не в два раза и будет вычисляться по специальной формуле в функции growslice().

      • Если развернуть ответ полностью, то это будет звучать примерно так:

        • если требуемая cap больше чем вдвое исходной cap, то новая cap будет равна требуемой;
        • если это условие не выполнено, а также len текущего слайса меньше 256, то новая cap будет в два раза больше базовой cap;
        • если первое и второе условия не выполнены, то емкость будет увеличиваться в цикле на четверть от базовой емкости пока не будет обработано переполнение. Посмотреть эти условия более подробно можно в исходниках go.

    • Вопрос №3: [ Как работает append? ]

      Ответ
      • Функция append в Go используется для добавления элементов в срез (slice). Эта функция возвращает новый срез, который содержит все элементы оригинального среза плюс новые элементы.

      • Как работает append под капотом? Срезы в Go — это динамические массивы, и они имеют три компонента: указатель на базовый массив, длину и вместимость. Когда вы используете append, происходят следующие вещи:

        • Если вместимость текущего среза достаточна для добавления новых элементов, они просто добавляются в срез, а функция append возвращает тот же срез (по той же ссылке).
        • Если вместимость текущего среза недостаточна для добавления новых элементов, Go создает новый массив с большей вместимостью и копирует в него все элементы из оригинального среза, а затем добавляет новые элементы. Функция append возвращает новый срез с новым базовым массивом.

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


  • Map
    • Вопрос №1: [ Как реализована map(карта) go? ]

      Ответ
      • Сама map в go - это структура, реализующая операции хеширования. При этом, так же как и любую структуру, содержащую ссылки на области памяти,map необходимо инициализировать. map ссылается на такие элементы как bucket (в переводе на русский "ведра"). Каждый bucket содержит в себе:

        • 8 экстра бит, с помощью которых осуществляется доступ до значений в этом bucket;
        • ссылку на следующий коллизионный bucket;
        • 8 пар ключ-значение, уложенных в массив.

    • Вопрос №2: [ Можно ли брать ссылку на значение, хранящееся по ключу в map? ]

      Ответ
      • Нельзя так как map поддерживает процедуру эвакуации. Значения, хранящиеся в определённой ячейки памяти в текущий момент времени, в следующий момент времени уже могут там не храниться.

    • Вопрос №3: [ Что такое эвакуация, и в каком случае она будет происходить? ]

      Ответ
      • Эвакуация - это процесс когда map переносит свои значения из одной области памяти в другую. Это происходит из-за того что число значений в каждом отдельном bucket максимально равно 8. В тот момент времени, когда среднее количество значений в bucket составляет 6.5, go понимает, что размер map не удовлетворяет необходимому. Начинается процесс расширения map. Следует отметить, что сам процесс эвакуации может происходить некоторое время, на протяжение которого новые и старые данные будут связаны.

    • Вопрос №4: [ Какие есть особенности синтаксиса получения и записи значений в map? ]

      Ответ
      • Получить значение из map, которую мы предварительно не аллоцировали нельзя, приложение упадет в панику. Если ключ не найден в map в ответ мы получим дефолтное значение для типа значений map. То есть, для строки - это будет пустая строка, для int - 0 и так далее. Для того, чтобы точно понять, что в map действительно есть значение, хранящееся по переданному ключу, необходимо использовать специальный синтаксис. А именно, возвращать не только само значение, но и булевую переменную, которая показывает удалось-ли получить значение по ключу.

    • Вопрос №5: [ Как происходит поиск по ключу в map? ]

      Ответ
      • Вычисляется хэш от ключа;
      • С помощью значения хэша и размера bucket вычисляется используемый для хранения bucket;
      • Вычисляется дополнительный хэш - это первые 8 бит уже полученного хэша;
      • В полученном bucket последовательно сравнивается каждый из 8 его дополнительных хэшей с дополнительным хэшем ключа;
      • Если дополнительные хэши совпали, то получаем ссылку на значение и возвращаем его;
      • Если дополнительные хэши не совпали, и в bucket больше нет дополнительных хэшей, алгоритм переходит в следующий bucket, ссылка на который хранится в текущем;
      • Если в текущем bucket нет ссылки на следующий bucket, а значение так и не найдено, возвращается дефолтное значение.

  • Defer
    • Вопрос №1: [ Зачем используется ключевое слово defer в go? ]

      Ответ
      • Ключевое слово defer используется для отложенного вызова функции. При этом, место объявления одной инструкции defer в коде никак не влияет на то, когда та выполнится. Функция с defer всегда выполняется перед выходом из внешней функции, в которой defer объявлялась.

    • Вопрос №2: [ Каков порядок возврата при использовании несколько функций с defer в рамках одной внешней функции? ]

      Ответ
      • defer добавляет переданную после него функцию в стэк. При возврате внешней функции, вызываются все, добавленные в стэк вызовы. Поскольку стэк работает по принципу LIFO (last in first out), значения стэка возвращаются в порядке от последнего к первому. Таким образом функции c defer будут вызываться в обратной последовательности от их объявления во внешней функции.

    • Вопрос №3: [ Как передаются значения в функции, перед которыми указано ключевое слово defer? ]

      Ответ
      • Аргументы функций, перед которыми указано ключевое слово defer оцениваются немедленно. То есть на тот момент, когда переданы в функцию.

  • Горутины
    • Вопрос №1: [ Что такое поток и горутина? ]

      Ответ
      • Поток (Thread) Поток — это базовая единица выполнения кода в операционной системе. Каждый поток имеет свой собственный стек и счетчик команд, но потоки из одного и того же процесса обычно разделяют ту же область памяти (кучу), переменные окружения и открытые файлы. Современные операционные системы, такие как Windows, macOS и Linux, поддерживают многопоточные процессы.

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

        Недостатки: Управление потоками и синхронизация между ними могут быть сложными. Проблемы, такие как "гонка" (race conditions), могут возникнуть, если необходимая синхронизация между потоками не реализована правильно.

      • Горутины — это абстракция, предоставляемая языком программирования Go, для создания легковесных потоков выполнения. Горутины работают на фоне операционных потоков, но управляются Go runtime, что делает их более легковесными и эффективными для многозадачности.

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

        Недостатки: Специфичны для языка Go и не могут быть использованы в других языках программирования без подобной абстракции.

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

        Процесс Изоляция: Процесс является полностью изолированной единицей выполнения с собственным адресным пространством и ресурсами. ОС: Управляется напрямую операционной системой. Затраты: Создание, уничтожение и контекстное переключение процессов являются дорогостоящими операциями. Коммуникация: Взаимодействие между процессами (IPC, Inter-Process Communication) обычно медленное и сложно настраивается. Примеры: Веб-сервер, база данных, браузер — каждый из них является отдельным процессом.

        Поток (Thread) Изоляция: Потоки внутри одного процесса разделяют адресное пространство и ресурсы, что упрощает коммуникацию между ними. ОС: Также управляется операционной системой, но легче и быстрее создавать и уничтожать по сравнению с процессами. Затраты: Меньше ресурсов требуется для создания, уничтожения и переключения контекста. Коммуникация: Быстрое взаимодействие между потоками за счет общего адресного пространства. Примеры: Потоки внутри веб-сервера, которые обрабатывают отдельные входящие соединения.

        Горутина (Goroutine) Изоляция: Горутины являются ещё более "легковесными" потоками, управляемыми средой исполнения Go, а не ОС. ОС: Управляется планировщиком в среде исполнения Go. Затраты: Очень дешевы в плане ресурсов. Создание, уничтожение и переключение контекста выполняются очень быстро. Коммуникация: Используют каналы и другие средства синхронизации Go для взаимодействия, что делает код более читаемым и поддерживаемым. Примеры: Отдельные задачи внутри веб-сервера на Go, работающие параллельно для обработки входящих запросов.

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


    • Вопрос №2: [ Сколько можно запустить потоков и горутин? ]

      Ответ
      • Потоки: Ограничено ресурсами системы, обычно несколько тысяч.
      • Горутины: Теоретически, десятки и сотни тысяч, зависит от ресурсов и конкретной задачи.

    • Вопрос №3: [ Каков минимальный и максимальный вес горутин? ]

      Ответ
      • На этот вопрос, ожидается ответ, не сколько весят все вместе взятые поля в структуре g объекта горутины. Интервьюера интересуют минимальный и максимальный размер стэка горутины. Минимальный (начальный) размер стэка составляет 2 КБ. Максимальный размер стэка горутины зависит от архитектуры системы и равен 1 ГБ для 64-разрядной архитектуры, 250 МБ для 32-разрядной архитектуры.

    • Вопрос №4: [ Что будет если размер горутины превысил допустимый максимум? ]

      Ответ
      • Если размер стэка горутины превышен (к примеру запустили бесконечную рекурсию), то приложение упадет с fatal error.

    • Вопрос №5: [ Какие есть способы остановить все горутины в приложении? ]

      Ответ
      • Если размышлять глобально, то таких способа 3:
        • завершение main функции и main горутины;
        • прослушивание всеми горутинами channel, при закрытии channel отправляется значение по умолчанию всем слушателям, при получении сигнала все горутины делают return;
        • завязать все горутины на переданный в них context.

    • Вопрос №6: [ Как наладить связь между горутинами? ]

      Ответ
      • Горутины общаются друг с другом посредством перегонки необходимых данных по channel. Именно о каналах идет речь в знаменитом девизе Go: "Не общайтесь, делясь памятью; делитесь памятью, общаясь".

  • Примитивы синхронизации
    • Вопрос №1: [ Какие есть примитивы синхронизации? Расскажи немного про каждый ]

      Ответ
      • wait group: sync.WaitGroup используется для ожидания завершения группы горутин. Это полезно, когда вы хотите дождаться завершения всех запущенных задач.

      • mutex: sync.Mutex и sync.RWMutex — это примитивы для обеспечения взаимоисключающего доступа к ресурсам. Mutex используется для обеспечения эксклюзивного доступа к критической секции кода.

      • atomic: предоставляет функции для выполнения атомарных операций на базовых типах данных, таких как int32, int64, uint32, uint64, uintptr, и указателях. Эти операции гарантируют, что изменение значения будет выполнено без прерываний, что полезно при высококонкурентном доступе к переменной.

      • sync map: sync.Map — это конкурентная (thread-safe) реализация карты, которая может быть безопасно использована несколькими горутинами без дополнительной блокировки. Обычные карты в Go не являются безопасными для использования в нескольких горутинах. Если вы пытаетесь одновременно читать и модифицировать карту из разных горутин, это может привести к неопределённому поведению. sync.Map решает эту проблему.

      • once: sync.Once предназначен для безопасного выполнения какой-либо операции только один раз, независимо от того, сколько горутин пытаются её выполнить.

      • channel: Каналы в Go — это мощный примитив для синхронизации и передачи данных между горутинами. Они могут быть использованы как очереди сообщений или как семафоры.


    • Вопрос №2: [ Что такое channel под капотом? ]

      Ответ
      • channel - это абстракция Go, которая помогает горутинам общаться друг с другом, передавая по channel значения. Канал можно представить как трубу, в которую одни горутины кладут данные, а другие их вычитывают. Под капотом channel представляет из себя 3 структуры (hchan, sudog, waitq). Наиболее интересной для нас является hchan, основные поля которой:

      • qcount - количество элементов в буфере;

      • dataqsiz - размерность буфера;

      • buf - указатель на буфер для элементов канала;

      • elemsize - размер элемента в канале;

      • closed - флаг, указывающий, закрыт канал или нет (1/0 соответственно);

      • elemtyp - тип элемента;

      • recvq - указатель на связанный список горутин, ожидающих чтения из канала;

      • sendq - указатель на связанный список горутин, ожидающих запись в канал;

      • lock - мьютекс для безопасного доступа к каналу. Когда мы создаем канал, мы присваеваем hchan elemtype и elemsize и аллоцируем структуру hchan в Heap.


    • Вопрос №3: [ Что такое буферизированный и не буферизированный channel? ]

      Ответ
      • channel делятся на два типа по наличию/отсутствию буфера. Соответственно в первом случае поле dataqsiz будет равно размеру переданного буфера (3), а поле buf будет ссылкой на этот буфер. Во втором случае поле dataqsiz будет равно 0, а поле buf будет nil. Отсюда возникает различное поведение этих типов channel при операциях с ними

    • Вопрос №4: [ Какие действия можно произвести с каналом? ]

      Ответ
      • С channel можно сделать 4 действия:
      • Создать канал
      • Записать что-то в канал
      • Что-то вычитать из канала
      • Закрыть канал

    • Вопрос №5: [ Что будет если писать/читать в nil channel? ]

      Ответ
      • Как мы смотрели ранее, канал - это структура, которую надо инициализировать. Если же мы этого не сделали и пишем в nil канал то, произойдет deadlockиfatal error(при условии всех спящих горутин), так как в исходниках Go идет проверка на nil. Точно такое же поведение будет при чтении из nil канала

    • Вопрос №6: [ Что будет если писать/читать в/из закрытый channel? ]

      Ответ
      • Запись в закрытый канал приведет к панике. Опять же из-за проверки флага в исходниках. При чтении из закрытого канала мы получим совсем другое поведение - значение из буфера, если оно есть, или дефолтное значение типа данных канала если буфер канала пуст

    • Вопрос №7: [ Как закрыть channel? Что с ним происходит? ]

      Ответ
      • Для закрытия канала предусмотрена функция close. Если упрощенно (опускаем блокировки), то при закрытии канала происходят следующие действия:
      • проверка, что канал инициализирован и не является nil (panic - если это не так);
      • проверка, что канал не закрыт (panic - если это не так);
      • поле close hchan выставляется в 1 (true);
      • отправка всем ожидающим чтения default value типа данных в канале;
      • ожидающие записи получают panic. Интересный момент, что так как закрытие канала не блокирует чтение канала, то данные из буфера канала можно вычитать и после его закрытия.

    • Вопрос №8: [ Какие есть инструкции для чтения из channel? ]

      Ответ
      • Из канала можно читать значения: присваивая их в переменную; прослушивая канал с помощью инструкции for range; прослушивая канал с помощью инструкции select case. Также следует обратить внимание, что чтение из закрытого канала отдает дефолтное значение типа данных канала. Поэтому существует возможность проверить, что при чтении получено значение из буфера. Для этого используется синтаксис со второй (bool) переменной val, ok := <- myChan.

    • Вопрос №9: [ Что будет если писать/читать в/из буферизированный channel? ]

      Ответ
      • Запись в буферизированный канал не является блокирующей операцией до тех пор, пока не заполнится буфер канала. После операция вызовет блокировку. Чтение из буферизированного канала не является блокирующим, если буфер канала не пуст. При пустом буфере канала чтение из него вызовет блокировку. Важный момент, что чтение из буферизированного канала - жадная операция. Если начался процесс чтения данных из канала, то данные будут читаться без блокировки до момента опустошения буфера.

    • Вопрос №10: [ Что будет если писать/читать в/из не буферизированный channel? ]

      Ответ
      • Не буферизированный канал - это тот же буферизированный канал, но с nil буфером. Соответственно принцип его работы будет таким же. Чтение из пустого и запись в непустой не буферизированный канал являются блокирующими операциями.

  • Switch/Select/Case
    • Вопрос №1: [ Как сделать select неблокирующим? ]

      Ответ
      • Есть возможность задать поведение для select по умолчанию, то есть для случаев, когда не выполняются case. Для этого необходимо добавить инструкцию default. Таким образом, когда не срабатывает ни один из case будет срабатывать кусок кода под инструкцией default.

    • Вопрос №2: [ Какой порядок исполнения операций case в select? ]

      Ответ
      • Первым выполнится тот case в select, который будет готов. При одновременной отправке данных в каналы, прослушиваемые в select порядок операций не гарантирован.

  • Context
    • Вопрос №1: [ Что такое context в GO? ]

      Ответ
      • По сути context - это некий сборник метаданных, который можно привязать к какому-нибудь процессу. К примеру для HTTP вызова можно объявить context, записать туда куки и иную информацию о пользователе. По окончанию вызова context можно отменить.

    • Вопрос №2: [ Для чего применяется context? ]

      Ответ
      • У context два основных применения:
      • Для отмены выполнения либо по таймауту, либо по дедлайну. Тот же пример с HTTP запросами;
      • Для передачи параметров. Правда злоупотребление этим плохо сказывается на явности кодовой базы. Обязательные параметры передавать через context все же не стоит.

    • Вопрос №3: [ Чем отличается context.Background от context.TODO? ]

      Ответ
      • И context.Background() и context.TODO() это одно и то же. Разница лишь в том, что context.TODO() выставляется в местах, где пока нет понимания, что необходимо использовать context.Background() и возможно его надо заменить на дочерний контекст.

    • Вопрос №4: [ Как передавать значения и вычитывать их из context? ]

      Ответ
      • В пакете context существует функция context.WithValue(parent Context, key, val interface{}) Context, которая от родительского контекста создает производный и добавляет в него по key значение. Извлекая значение из context необходимо помнить, что на выход получаем интерфейс, который необходимо правильно скастить.

    • Вопрос №5: [ Каковы отличия context.WithCancel, context.WithDeadline, context.WithTimeout? ]

      Ответ
      • context.WithCancel(parent Context) (ctx Context, cancel CancelFunc) создает контекст производный от родительского, также возвращает функцию отмены, с помощью которой этот контекст можно закрыть. Общепринятой практикой является работать с функцией отмены там, где она получена, не передавая ее глубже.
      • context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc) создает контекст производный от родительского, также возвращает функцию отмены, с помощью которой этот контекст можно закрыть. Контекст автоматически отменится в переданное, как входной параметр функции, время.
      • context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc) создает контекст производный от родительского, также возвращает функцию отмены, с помощью которой этот контекст можно закрыть. Контекст автоматически отменится через интервал времени, переданный, как входной параметр функции.

    • Вопрос №6: [ Как обрабатывать отмену context? ]

      Ответ
      • Отмену контекста можно обрабатывать через канал <-context.Done(), который уведомляет об отмене контекста.

  • Garbage Collector
    • Вопрос №1: [ Что такое сборщик мусора и по какому алгоритму он реализован в Go? ]

      Ответ
      • Любую аллоцированную память необходимо очищать после окончания ее использования. В некоторых языках программирования разработчик сам должен управлять этим процессом. В Go неиспользуемые объекты находит и удаляет сборщик мусора. Сборщик мусора - устроен по алгоритму Mark and Sweep

    • Вопрос №2: [ Расскажите про алгоритм mark and sweep ]

      Ответ
      • Алгоритм Mark and Sweep состоит из двух частей:
        • Mark разметка.
        • Sweep очистка памяти. Сама стадия Mark реализована с помощью 3 цветного алгоритма. Для наглядности представим, что все наши данные лежат в виде графа, все узлы графа помечаем белым цветом. Алгоритм: идет сканирование объектов первого уровня доступа, тех которые хранятся либо глобально, либо в стэке потока; объекты первого уровня помечаются серым цветом; в каждом сером объекте ищутся ссылки на области памяти; объекты по ссылкам помечаются серым; сам родительский элемент помечается черным; процесс повторяется, пока не останется серых объектов (белые объекты будем удалять на следующем шаге).

    • Вопрос №3: [ Когда запускается сборщик мусора? ]

      Ответ
      • По умолчанию сборщик мусора запускается в тот момент, когда heap увеличился вдвое. Этот параметр также можно настроить с помощью переменной среды окружения GOGC. Вручную сборщик мусора можно запустить с помощью runtime.GC()

    • Вопрос №4: [ Сколько ресурсов потребляет сборщик мусора? ]

      Ответ
      • Сборщик мусора потребляет до 25% CPU для фазы Mark. Помимо этого за цикл работы сборщика мусора два раза происходит остановка приложения (вызов stop the world).

  • ООП в Golang
    • Вопрос №1: [ Как реализовано ООП в golang? ]

      Ответ
      • Расскажи про наследование в Go? В Go нет наследования. Но есть встраивание (композиция). В Go нет классов, но структуры (struct) можно использовать для определения типов, которые хранят данные. Композиция позволяет включить одну структуру в другую, предоставляя возможность использовать поля и методы вложенной структуры.

      • Расскажи про пнкапсуляция в Go? Инкапсуляция в go - это возможность задавать переменным, функциям и методам первую букву названия в верхнем или нижнем регистре. Соответственно нижний регистр будет значить, что переменная, функция или метод доступна только в рамках пакета. Тогда как верхний регистр даст доступ к переменной, функции или методу за рамками пакета.

      • Расскажи про полиморфизм в Go? Полиморфизм в go реализован с помощью интерфейсов. Основная идея заключается в том, что мы можем объявить интерфейсы (контракты на определённое поведение) для наших типов. При этом, для типов мы должны реализовать методы, удовлетворяющие этим интерфейсам. Таким образом, мы сможем работать со всем набором типов, у которых реализовали интерфейсы, как с единым интерфейсным типом.


  • Ошибки и паники Golang
    • Вопрос №1: [ Обработка ошибок в go, есть ли исключения, как работать с panic? ]

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

        Panic и Recover Хотя исключений нет, в Go есть механизмы panic и recover, которые используются для обработки и восстановления после критических ошибок (обычно это ошибки, которые программист не предвидел или не может корректно обработать).

      • "panic" останавливает нормальное выполнение функций и начинает прокладывать путь обратно по стеку вызовов, выполняя при этом defer-вызовы.

      • "recover" используется для перехвата значения, переданного panic, должен быть вызван внутри defer он возвращает значение паники, после этого паника прекращается, и выполнение программы продолжается с инструкции, следующей за вызовом паничной функции, которая привела к панике

        Принципы обработки ошибок в Go

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


Практические задачи Golang
  • Горутины
    • Вопрос №1: [ Что выведет код? (Горутины без синхронизации) ] Static Badge

      Код
      package main
      
      import (
        "fmt"
        "time"
      )
      
      func main() {
        for i := 0; i < 3; i++ {
          go func(i int) {
              fmt.Println(i)
          }(i)
        }
      }
      Ответ
      • Пояснение: Так как мы запускаем горутины без примитивов синхронизации то функция main выполнится быстрее чем горутины не дожидаясь их выполнения.
      • Ответ: ничего не выведется

    • Вопрос №2: [ Что выведет код? (Захват переменной) ] Static Badge

      Код
      package main
      
      import (
        "fmt"
        "time"
      )
      
      func main() {
          for i := 0; i < 3; i++ {
            go func() {
                fmt.Println(i)
            }()
          }
          time.Sleep(100 * time.Millisecond)
      }
      Ответ
      • Пояснение: Здесь мы не передает значение i аргументом в горутину. Все горутины захватывают переменную цикла. На момент запуска горутин она уже будет равна 3 и все три горутины будут работать с ней.
      • Ответ: 3 3 3

    • Вопрос №3: [ Что выведет код? (Select case) ] Static Badge

      Код
      package main
      
      import (
        "fmt"
        "time"
      )
      
      func main() {
        ch := make(chan int)
      
        go func() {
            ch <- 1
            time.Sleep(100 * time.Millisecond)
            ch <- 2
        }()
      
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-time.After(50 * time.Millisecond):
            fmt.Println("Timeout")
        }
      }
      Ответ
      • Пояснение: В этом коде используется оператор select, который ожидает первый завершившийся канал из списка и выполняет соответствующий ему блок кода. После этого select завершает свою работу. Он не ожидает, пока все каналы завершат свою работу или отправят значения.
      • Ответ: 1

    • Вопрос №4: [ Что выведет код? (Deadlock или нет) ] Static Badge

      Код
      package main
      
      import (
        "fmt"
      )
      
      func main() {
        ch := make(chan int, 1)
        go func() {
          for i := 0; i < 2; i++ {
            select {
            case ch <- i:
            }
          }
          close(ch)
        }()
      
        for i := range ch {
          fmt.Println(i)
          }
      }
      Ответ
      • Пояснение: Создается канал ch с буфером размером 1. Горутина начинает с отправки значения 0 в канал. Канал может хранить одно значение, так что это значение успешно отправляется. Затем она пытается отправить значение 1. Но канал уже полон (в нем уже есть значение 0), так что горутина блокируется, ожидая, пока канал освободится. Затем цикл for i := range ch начинает читать из канала. Как только он читает значение 0, канал освободится. Теперь гоуртина она может отправить значение 1 в канал, так как он теперь пуст. В цикле продолжает читать из канала и читает значение 1. Горутина закрывает канал ch. Выходим из цикла for i := range ch, так как канал был закрыт.
      • Ответ: 0 1

    • Вопрос №5: [ Что выведет код? (Горутины и каналы) ] Static Badge

      Код
      package main
      
      import (
        "fmt"
        "time"
      )
      
      func main() {
        ch1 := make(chan int)
        ch2 := make(chan int)
        ch3 := make(chan int)
      
        go func() {
            for i := 0; i < 3; i++ {
                ch1 <- i
                time.Sleep(100 * time.Millisecond)
            }
            close(ch1)
        }()
      
        go func() {
            for i := 3; i < 6; i++ {
                ch2 <- i
            }
            close(ch2)
        }()
      
        go func() {
            for val := range ch1 {
                ch3 <- val
            }
            for val := range ch2 {
                ch3 <- val
            }
            close(ch3)
        }()
      
        for val := range ch3 {
            fmt.Println(val)
        }
      }
      Ответ
      • Пояснение: Первая горутина добавляет элементы в ch1 с задержкой, что дает второй горутине время на то, чтобы полностью заполнить ch2 до того, как первая горутина закроет ch1. Третья горутина начнет с чтения из ch1 и будет ждать, пока первая горутина закроет ch1. Затем она начнет читать из ch2. К моменту, когда третья горутина начнет читать из ch2, вторая горутина уже закроет ch2, и все значения будут прочитаны и добавлены в ch3. Главная горутина читает значения из ch3 в том порядке, в каком они были добавлены. Сначала это будут значения из ch1 (0, 1, 2), а затем из ch2 (3, 4, 5).
      • Ответ: 0 1 2 3 4 5

    • Вопрос №6: [ Что выведет код? (Горутины и каналы) ] Static Badge

      Код
      package main
      
      import (
      "fmt"
      "sync"
      )
      
      func main() {
          m := make(chan string, 3)
          var wg sync.WaitGroup
      
          wg.Add(5)
          for i := 0; i < 5; i++ {
              i := i
              go func() {
                  defer wg.Done()
                  m <- fmt.Sprintf("goroutine %d", i)
              }()
          }
      
          wg.Wait()
          
          for i := range m {
          	fmt.Println(i)
          }
      }
      Ответ
      • Пояснение:
        На первый взгляд код выглядит рабочим, но здесь есть несколько проблем: Мы создаем буферизованный канал емкостью 3 а затем запускаем 5 горутин пытающихся записать в этот канал.
        3 горутины запишут успешно, а 2 заблокируются до момента пока из канала не прочтут информацию.
        Так как мы используем wg.Wait() код будет ждать выполнения всех 5 горутин и цикл на чтение из канала не запустится и мы получим deadlock.
        Второй момент: даже если мы исправим ситуацию с ожиданием горутин. Мы все равно получим deadlock так как мы не закрываем канал

      • Ответ: deadlock


  • Замыкание
    • Вопрос №1: [ Что выведет код? (Типичное замыкание) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        funcs := []func(){}
        for i := 0; i < 3; i++ {
          funcs = append(funcs, func() {
              fmt.Println(i)
          })
        }
        for _, f := range funcs {
          f()
        }
      }
      Ответ
      • Пояснение: Все функции в срезе funcs на самом деле "замыкают" одну и ту же переменную i, которая изменяется на каждой итерации основного цикла. К моменту выполнения второго цикла for, значение i становится равным 3 (так как индексы начинаются с 0, и цикл идет до момента, когда i < 3). Таким образом, при вызове каждой функции из среза funcs, будет выведено 3. Этот эффект часто называют "ловушкой замыкания в цикле", и он может быть источником путаницы или ошибок. Чтобы избежать этого, можно использовать локальную переменную внутри цикла или использовать "фабрику функций", как показано в одной из задач этой категории.
      • Ответ: 3 3 3

    • Вопрос №2: [ Что выведет код? (Замыкание и изменение переменной) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        x := 10
        f := func() {
          fmt.Println(x)
        }
        x += 10
        f()
      }
      Ответ
      • Пояснение: В этой задаче f является замыканием, которое замыкает переменную x. Когда значение x изменяется после объявления f, замыкание также "видит" это новое значение. Поэтому когда f() вызывается, оно выводит 20.
      • Ответ: 20

    • Вопрос №3: [ Что выведет код? (Фабрика функций) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func createClosure(i int) func() {
        return func() {
        fmt.Println(i)
        }
      }
      
      func main() {
        closures := []func(){}
        for i := 0; i < 3; i++ {
          closures = append(closures, createClosure(i))
        }
        for _, closure := range closures {
          closure()
        }
      }
      Ответ
      • Пояснение: В этом случае каждая функция создаётся с собственным значением i, поэтому замыкания выводят разные значения.
      • Ответ: 0 1 2

    • Вопрос №4: [ Что выведет код? (Замыкание и изменение состояния) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        sum := 0
        adder := func(i int) {
          sum += i
        }
        adder(10)
        adder(20)
        fmt.Println(sum)
      }
      Ответ
      • Пояснение: Здесь adder — это функция, которая изменяет внешнюю переменную sum. Каждый вызов adder(i) увеличивает sum на i. Таким образом, замыкание может использоваться для изменения и хранения состояния.
      • Ответ: 30

  • Map
    • Вопрос №1: [ Что выведет код? (Инициализация и добавление элементов) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        m := map[string]int{}
        m["a"] = 1
        m["b"] = 2
        fmt.Println(len(m))
      }
      Ответ
      • Пояснение: В этой задаче создаётся пустая карта m с ключами типа string и значениями типа int. Затем в эту карту добавляются два элемента: "a": 1 и "b": 2. Функция len() возвращает количество элементов в карте.
      • Ответ: 2

    • Вопрос №2: [ Что выведет код? (Поиск в карте) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        m := map[string]int{
        "a": 1,
        "b": 2,
        }
        val, ok := m["c"]
        fmt.Println(val, ok)
      }
      Ответ
      • Пояснение: Здесь создаётся карта m с двумя элементами. Потом происходит попытка получить значение по несуществующему ключу "c". В Go, если ключа нет в карте, то будет возвращено нулевое значение для типа хранящихся в карте значений (в данном случае это 0 для типа int) и флаг ok, указывающий на существование ключа, будет равен false.
      • Ответ: 0 false

    • Вопрос №3: [ Что выведет код? (Удаление элемента) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        m := map[string]int{
        "a": 1,
        "b": 2,
        }
        delete(m, "a")
        fmt.Println(len(m))
      }
      Ответ
      • Пояснение: В этой задаче создаётся карта m с двумя элементами. Затем один из них удаляется с помощью функции delete(). Количество элементов после этого операции станет равным 1.
      • Ответ: 1

    • Вопрос №4: [ Что выведет код? (Изменение значения по ключу) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        m := map[string]int{
        "a": 1,
        }
        m["a"] = 2
        fmt.Println(m["a"])
      }
      Ответ
      • Пояснение: Пояснение: Задача демонстрирует, как можно изменить значение по уже существующему ключу в карте. Изначально в карте есть один элемент с ключом "a" и значением 1. Затем это значение перезаписывается на 2.
      • Ответ: 2

    • Вопрос №5: [ Что выведет код? (Ключи с нулевыми значениями) ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        m := map[string]int{
        "a": 0,
        }
        val, ok := m["a"]
        fmt.Println(val, ok)
      }
      Ответ
      • Пояснение: Эта задача показывает, что в карте могут храниться ключи с нулевыми значениями. При попытке получить значение по ключу "a" вернётся 0 и флаг ok будет равен true, указывая на то, что такой ключ действительно существует в карте. А если бы ключа не было, то флаг ok был бы false
      • Ответ: 0 true

  • Слайсы и массивы
    • Вопрос №1: [ Что выведет код? (Изменение элементов слайса)] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        a := []int{1, 2, 3}
        b := a[:1]
        b[0] = 5
        fmt.Println(a[0], a[1])
      }
      Ответ
      • Пояснение:
        Сначала создается срез a с тремя элементами: [1, 2, 3]. Затем создается срез b, который получен из a и содержит только первый элемент среза a. В памяти эти срезы указывают на один и тот же массив, таким образом, изменение в b приведет к изменению в a. Значение первого элемента в срезе b устанавливается равным 5. Это также изменит первый элемент в срезе a, так как b является подсрезом a.
      • Ответ: 5 2

    • Вопрос №2: [ Что выведет код? (Изменение элементов массива)] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        arr := [3]int{1, 2, 3}
        slice := arr[:2]
        slice[0] = 4
        fmt.Println(arr[0])
      }
      Ответ
      • Пояснение:
        В этом коде создается массив arr и срез slice, который ссылается на первые два элемента массива arr. Затем изменяется первый элемент среза slice. Поскольку slice ссылается на часть массива arr, изменение в slice отразится и на массиве arr.
      • Ответ: 4

    • Вопрос №3: [ Что выведет код? (Длина и емкость среза)] Static Badge

      Код
      package main
      
      import "fmt"
      
        func main() {
        a := []int{1, 2, 3}
        b := a[1:3]
        b = append(b, 4)
        fmt.Println(len(a), cap(a))
      }
      Ответ
      • Пояснение:
        Срез b создается на основе среза a и включает в себя элементы с индексами 1 и 2. Затем к срезу b добавляется элемент 4. Однако это не изменяет длину или емкость исходного среза a.
      • Ответ: 3 3

    • Вопрос №4: [ Что выведет код? (Срезы и make)] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        a := make([]int, 2, 4)
        a[0] = 1
        a[1] = 2
        a = append(a, 3, 4)
        a = append(a, 5)
        fmt.Println(cap(a))
      }
      Ответ
      • Пояснение:
        Срез a создается с длиной 2 и емкостью 4. Затем добавляются элементы, заполняя его полностью. Когда добавляется ещё один элемент (5), срезу потребуется больше места, и Go создаст новый, больший срез. В данном случае, емкость удваивается.
      • Ответ: 8

    • Вопрос №5: [ Что выведет код? (Нулевой срез и nil)] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        var a []int
        b := []int{}
        fmt.Println(a == nil, b == nil)
      }
      Ответ
      • Пояснение:
        Переменная a является нулевым срезом и равна nil. Срез b, хотя и пуст, но не равен nil.
      • Ответ: true false

    • Вопрос №6: [ Что выведет код? (Изменение среза в функции)] Static Badge

      Код
      package main
      
      import "fmt"
      
      func modify(s []int) {
        if len(s) > 0 {
        s[0] = 100
        }
      }
      
      func main() {
        a := []int{1, 2, 3}
        modify(a)
        fmt.Println(a[0])
      }
      Ответ
      • Пояснение:
        Срезы в Go являются ссылочным типом данных. Это значит, что при передаче среза в функцию и его изменении внутри этой функции, будет изменен и исходный срез.
      • Ответ: 100

  • Указатели
    • Вопрос №1: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        a := 5
        b := &a
        *b = 10
        fmt.Println(a)
      }
      Ответ
      • Пояснение: В этой задаче у нас есть переменная a со значением 5. Затем мы создаём указатель b, который указывает на переменную a. Изменяя значение по этому указателю на 10, мы изменяем и саму переменную a.
      • Ответ: 10

    • Вопрос №2: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func modify(x *int) {
          *x += 5
      }
      
      func main() {
        a := 2
        modify(&a)
        fmt.Println(a)
      }
      Ответ
      • Пояснение: Здесь функция modify принимает указатель на int и добавляет к нему 5. В main мы передаём указатель на переменную a в эту функцию. По этой причине значение a становится равным 7 (2+5).
      • Ответ: 7

    • Вопрос №3: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        a := 1
        p1 := &a
        p2 := &a
        *p1 = 3
        *p2 = 4
        fmt.Println(a, *p1, *p2)
      }
      Ответ
      • Пояснение: В этой задаче переменная a равна 1, и мы создаём два указателя (p1 и p2), которые указывают на a. Затем мы меняем значение a через эти указатели. Последний указатель, который меняет значение, устанавливает его в 4.
      • Ответ: 4 4 4

    • Вопрос №4: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      type Node struct {
          Value int
          Next  *Node
      }
      
      func reverse(head **Node) {
          var prev *Node
          current := *head
          for current != nil {
              next := current.Next
              current.Next = prev
              prev = current
              current = next
          }
          *head = prev
      }
      
      func main() {
          third := &Node{3, nil}
          second := &Node{2, third}
          first := &Node{1, second}
      
          reverse(&first)
          fmt.Println(first.Value)
          fmt.Println(first.Next.Value)
          fmt.Println(first.Next.Next.Value)
      }
      Ответ
      • Пояснение: Эта задача демонстрирует обращение односвязного списка. Функция reverse изменяет направление ссылок в списках. Изначально список имеет вид 1->2->3. После обращения он становится 3->2->1.
      • Ответ: 3 2 1

    • Вопрос №5: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
        x := 0
        y := 5
      
        p := &x
        pp := &p
      
        *(*(*(&pp)))++
        y /= *p
        fmt.Println(y)
      }
      Ответ
      • Пояснение: Эта задача демонстрирует использование указателей и двойных указателей в необычной манере. Изначально x равно 0, и y равно 5. В строке *(*(*(&pp)))++, pp является двойным указателем на x. Эта строчка увеличивает значение x на 1, делая его равным 1. Затем y делится на x
      • Ответ: 5

    • Вопрос №6: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import (
          "fmt"
          "unsafe"
      )
      
      type st struct {
          p1 bool
          p2 int
          p3 bool
      }
      
      func main() {
          myStr := st{}
      
          fmt.Println(unsafe.Sizeof(myStr)) // 1?
      
          fmt.Println(myStr) // 2?
      
          mutatePtr1(&myStr)
      
          fmt.Println(myStr) // 4?
      
          mutatePtr2(&myStr)
      
          fmt.Println(myStr) // 6?
      
      }
      
      func mutatePtr1(in *st) {
          in = &st{
              p1: true,
              p2: 666,
              p3: false,
          }
      
          fmt.Println("in mutatePtr1", in) // 3?
      }
      
      func mutatePtr2(in *st) {
          *in = st{
              p1: false,
              p2: 8841,
              p3: true,
          }
      
      	fmt.Println("in mutatePtr2", in) // 5?
      }
      Ответ
      • Пояснение:
        fmt.Println(unsafe.Sizeof(myStr)) // 1?
        Выводит размер структуры myStr в байтах. Это значение зависит от реализации и архитектуры системы, но обычно это 16 байтов (1 байт для каждого bool и 8 байтов для int, плюс дополнительные байты для выравнивания).
        fmt.Println(myStr) // 2?
        Выводит нулевые значения полей структуры (false для bool, 0 для int).
        mutatePtr1(&myStr)
        Вызывается функция mutatePtr1, которая принимает указатель на структуру и создает новый экземпляр этой структуры. Однако это не меняет оригинальную структуру, поскольку указатель in внутри функции mutatePtr1 теперь указывает на новую структуру.
        fmt.Println(myStr) // 4?
        Показывает, что оригинальная структура myStr не изменилась, так как mutatePtr1 не меняет ее.
        mutatePtr2(&myStr)
        Вызывается функция mutatePtr2, которая принимает указатель на структуру и меняет поля этой структуры. В этом случае изменения затрагивают оригинальную структуру myStr.
        fmt.Println(myStr) // 6?
        Показывает, что оригинальная структура myStr изменилась, так как mutatePtr2 изменил ее.
      • Ответ:
        24
        {false 0 false}
        in mutatePtr1 &{true 666 false}
        {false 0 false}
        in mutatePtr2 &{false 8841 true}
        {false 8841 true}

    • Вопрос №7: [ Что выведет код? ] Static Badge

      Код
      package main
      
      import "fmt"
      
      func main() {
          var numbers []*int
      
          for _, value := range []int{10, 20, 30, 40} {
              numbers = append(numbers, &value)
          }
      
          for _, number := range numbers {
              fmt.Println(*number)
          }
      }
      Ответ
      • Пояснение:
        В этом коде есть ловушка: так как value является одной и той же переменной во время каждой итерации цикла, а мы добавляем в слайс numbers именно указатель на value, то все указатели в срезе numbers будут указывать на последнее значение value (в данном случае 40).

      • Ответ: 40 40 40 40



Python
  • Общие вопросы по языку Python
    • Вопрос №1: [ Расскажи кратко о языке Python ]

      Ответ
      • Python — это высокоуровневый императивный язык программирования, который обладает рядом характерных особенностей:
        • Читаемость: Python поощряет написание читаемого кода с помощью принудительного использования отступов.
        • Многопарадигмальный: Python поддерживает несколько парадигм программирования, включая процедурное, объектно-ориентированное и функциональное программирование.
        • Стандартная библиотека: Python имеет обширную стандартную библиотеку, которая предлагает инструменты для многих задач, начиная от работы с файлами и заканчивая веб-серверами.
        • Динамическая типизация: В Python переменные не требуют явного указания типа, и тип может меняться в процессе выполнения.
        • Управление памятью: Python автоматически управляет памятью с помощью сборщика мусора.
        • Расширяемость: Python может быть расширен с помощью модулей, написанных на C или C++.
        • Большие возможности для интеграции: Python может вызывать библиотеки C/C++, можно вызывать код Python из C/C++, и он имеет поддержку для вызова процедур из других языков.
        • Портативность: Python доступен для большинства операционных систем, и код, написанный на одной платформе, зачастую может быть без изменений выполнен на другой (с учетом платформозависимых особенностей).
        • Коммьюнити: У Python очень активное и поддерживающее сообщество, что способствует быстрому решению проблем и обучению.
        • Применение: Python широко используется в веб-разработке, научных вычислениях, анализе данных, автоматизации, тестировании, искусственном интеллекте и многих других областях.
        • Интерпретируемый: Python — это интерпретируемый язык, что означает, что для его выполнения требуется интерпретатор Python, в отличие от компилируемых языков, где код сначала преобразуется в машинный код.

    • Вопрос №2: [ Какие типы данных существуют в Python и на какие группы их можно разделить? ]

      Ответ
      • В Python представлены разнообразные типы данных. Они могут быть разделены на следующие группы:
      • Простые типы данных (или базовые):
        int: Целые числа (например, 1, -5, 1000).
        float: Вещественные числа (например, 3.14, -2.71, 0.0).
        complex: Комплексные числа (например, 1+2j).
        bool: Булев тип (True или False).
        str: Строки ("Hello", 'Python').
        bytes: Последовательности байтов.
        bytearray: Изменяемые последовательности байтов.
        memoryview: Объекты "представления памяти".
      • Составные (или контейнерные) типы данных:
        list: Списки (например, [1, 2, 3] или ["apple", "banana"]).
        tuple: Кортежи (например, (1, 2, 3) или ("apple", "banana")).
        set: Множества (например, {1, 2, 3}).
        frozenset: Неизменяемые множества.
        dict: Словари, или ассоциативные массивы (например, {"key1": "value1", "key2": "value2"}).
      • Типы для работы с файлами:
        file: Для работы с внешними файлами.
      • Типы None:
        NoneType: Имеет единственное значение None, которое обычно используется для обозначения отсутствия значения или как заглушка.
      • Пользовательские типы:
        Можно определять собственные типы данных с помощью классов.
      • Специальные типы:
        Например, модули, функции и методы также являются типами данных в Python, хотя они не всегда рассматриваются как "традиционные" типы данных.
      • Помимо этих основных типов данных, Python также поддерживает множество модулей и библиотек, которые предоставляют дополнительные типы данных для решения специфических задач.

    • Вопрос №3: [ Как реализовано хранилище памяти в Python? ]

      Ответ
      • Python использует собственную систему управления памятью, которая состоит из нескольких компонентов:
      • Приватное хранилище памяти: Python осуществляет выделение памяти из приватного хранилища для всех своих объектов и структур данных. Этот приватный механизм недоступен для программиста на C или C++.
      • Сборщик мусора: Python оснащен встроенным сборщиком мусора, который отслеживает все ссылки на объекты. Как только какой-либо объект перестает быть доступным, память, занятая этим объектом, освобождается. Python использует подсчет ссылок и генерационный сбор мусора.
      • Система подсчета ссылок: Это один из инструментов, который использует Python для управления памятью. Каждый объект в Python содержит счетчик ссылок, который увеличивается, когда на объект создается ссылка, и уменьшается, когда ссылка уничтожается. Когда счетчик ссылок достигает нуля, память автоматически освобождается.
      • Бассейны памяти: Для эффективности управления памятью Python использует систему, известную как "бассейны памяти" (memory pools). Это означает, что память для небольших объектов (обычно объекты размером менее 512 байт) выделяется из "бассейнов" — больших блоков памяти, которые делятся на однородные части.
      • Малофрагментированное выделение: Для тонкого управления памятью, особенно для небольших размеров выделения, Python использует механизм, называемый "PyMalloc", который минимизирует фрагментацию и эффективно управляет выделением памяти для маленьких объектов.
      • Расширяемость: Python позволяет расширять управление памятью с помощью пользовательских аллокаторов, позволяя разработчикам определять свои собственные стратегии управления памятью при необходимости.
      • Несмотря на эффективные механизмы управления памятью, Python (особенно CPython) иногда критикуют за свое потребление памяти, особенно в сравнении с языками, такими как C или C++. Однако многие из этих механизмов разработаны для оптимизации быстродействия и удобства разработки, а не для минимизации использования памяти.

    • Вопрос №4: [ Что такое декоратор? ]

      Ответ
      • Декораторы в Python — это мощный и гибкий инструмент, позволяющий изменять или расширять функциональность функций или классов без изменения их исходного кода. Основная идея декораторов заключается в том, чтобы взять одну функцию и добавить "обертку" вокруг неё, модифицировав её поведение.
      • Декоратор — это функция, которая принимает другую функцию в качестве аргумента и возвращает новую функцию, обычно с добавленной или измененной функциональностью.

    • Вопрос №5: [ Что такое Лямбда функция? ]

      Ответ
      • Лямбда-функция в Python — это анонимная функция, созданная с помощью ключевого слова lambda. Она может иметь любое количество аргументов, но может содержать только одно выражение. Это выражение вычисляется и возвращается при вызове лямбда-функции. Лямбда-функции используются, когда необходима простая функция для короткого периода времени и не требуется полноценное именованное определение функции.

    • Вопрос №6: [ Как работают функции map, filter и reduce? ]

      Ответ
      • Функции map, filter и reduce — это встроенные функции высшего порядка в Python, предназначенные для работы с итерируемыми объектами. Они позволяют применять функции к каждому элементу итерируемого объекта (например, списка) или комбинировать их элементы определенным образом.
        • map:
          Применяет заданную функцию к каждому элементу итерируемого объекта. Возвращает новый итератор с результатами. Синтаксис: map(function, iterable, ...)
        • filter:
          Отфильтровывает элементы итерируемого объекта на основе заданной функции, возвращая только те, для которых функция возвращает True. Возвращает новый итератор с отфильтрованными результатами. Синтаксис: filter(function, iterable)
        • reduce:
          Применяет заданную функцию к элементам итерируемого объекта, поочередно "сжимая" их в одно общее значение. Не является встроенной функцией в Python 3, нужно импортировать из модуля functools. Синтаксис: reduce(function, iterable[, initializer])

    • Вопрос №7: [ Каковы основные проблемы, связанные с глобальной блокировкой интерпретатора (GIL)? ]

      Ответ
      • Глобальная блокировка интерпретатора (GIL, Global Interpreter Lock) — это механизм, используемый в CPython (основной и наиболее популярной реализации Python) для синхронизации выполнения потоков таким образом, чтобы только один поток мог выполнять инструкции Python в данный момент времени. Это неотъемлемая часть CPython и позволяет избежать проблем с многопоточным доступом к объектам Python. Но у GIL есть ряд недостатков:
      • Многопоточность: Даже на многоядерных процессорах GIL ограничивает выполнение многопоточных программ, написанных на Python, таким образом, что только один поток может выполнять инструкции Python одновременно. Это означает, что чисто вычислительные задачи в многопоточной программе на Python не будут эффективно распределены по ядрам и не получат преимущества от многопоточности.
      • Сложность разработки: Из-за GIL становится сложнее писать конкурентный и параллельный код, так как разработчикам нужно постоянно думать о GIL и его ограничениях.
      • Проблемы производительности: В некоторых случаях GIL может вызывать проблемы с производительностью из-за частого переключения контекста и блокирования ресурсов.
      • Сложности с расширениями: При написании расширений для Python на C или C++ разработчикам нужно учитывать GIL, особенно если их код может выполняться в многопоточной среде.
      • Задержки и неопределенность: GIL может вызывать неожиданные задержки в программе, особенно когда один поток ожидает, пока другой поток освободит GIL.
      • Стоит отметить, что GIL — это особенность CPython и не присутствует в других реализациях Python, таких как Jython (Python на JVM) или IronPython (Python на .NET). Кроме того, для многих задач GIL не является проблемой. Например, многопоточные I/O-операции или программы, в которых многопоточность используется для обработки событий или пользовательского интерфейса, могут не сталкиваться с проблемами GIL. Тем не менее, для интенсивных вычислительных задач или приложений, которые действительно должны максимально эффективно использовать все ядра процессора, GIL может стать проблемой, и в этом случае стоит рассмотреть другие реализации Python или методы параллелизма, такие как многопроцессорность.

    • Вопрос №8: [ Что такое init.py и какова его роль в пакетах Python? ]

      Ответ
      • В Python файл init.py играет особую роль в организации пакетов. Вот основные моменты, связанные с этим файлом:

        • Инициализация пакета: Наличие файла init.py сообщает интерпретатору Python, что директория должна рассматриваться как пакет или модуль. Это означает, что при импорте директории вам не нужно указывать расширение .py.
        • Запускаемый код при импорте: Когда вы импортируете пакет, код в файле init.py выполняется. Это может быть полезно для инициализации переменных пакета или для выполнения другого инициализирующего кода.
        • Определение содержимого пакета: Вы можете определить, какие модули будут доступны для импорта, когда пользователь импортирует * из пакета, устанавливая переменную all в файле init.py.
        • Сокрытие деталей реализации: Файл init.py позволяет вам скрыть внутренние модули и предоставить удобный интерфейс для использования вашего пакета.
        • Cовместимость: В более ранних версиях Python наличие файла init.py было обязательным для определения пакета. Начиная с Python 3.3, благодаря механизму пространств имён, пакеты не обязательно должны содержать файл init.py. Однако его наличие обеспечивает совместимость с более ранними версиями Python.
        • Субпакеты: Если ваш пакет имеет подпакеты, каждый из них также должен содержать файл init.py (даже если он пуст), чтобы быть признанным как пакет или подпакет.
      • В целом, файл __init__.py обеспечивает упорядоченное и структурированное организацию модулей и пакетов в Python, делая код более модульным и легко масштабируемым.


  • Строки
    • Вопрос №1: [ Что такое строка и какие манипуляции можно провести с ней? ]

      Ответ
      • Строки в Python — это неизменяемые последовательности символов. В Python для работы со строками предоставляется множество встроенных функций и методов.
      • Основные особенности и понятия, связанные со строками в Python:

      • Создание строк: Строки могут быть заключены в одинарные ('...'), двойные ("...") или тройные ('''...''' или """...""") кавычки. Тройные кавычки используются для многострочных строк.

      • Экранирование символов: Символ обратного слеша \ используется для экранирования специальных символов, например: '\n' (новая строка) или '\t' (табуляция).

      • Неизменяемость: Строки в Python неизменяемы, что означает, что после создания строки вы не можете изменить отдельные символы в ней. Любая операция, которая кажется изменяющей строку, на самом деле создает новую строку.

      • Индексация и срезы: Строки поддерживают индексацию и срезы. Например, s[0] вернет первый символ строки s, а s[1:4] вернет подстроку, начиная с второго и заканчивая четвертым символом.

      • Методы строк: Строки имеют множество встроенных методов, таких как split(), join(), replace(), upper(), lower() и многих других.

      • Форматирование строк: В Python предоставляется несколько способов форматирования строк, включая старый стиль с использованием оператора %, метод str.format() и, начиная с Python 3.6, f-строки, которые позволяют вставлять выражения напрямую в строковые литералы.

      • Юникод: В Python 3 все строки представляют собой последовательности символов Юникода. Это позволяет работать со строками на множестве языков без необходимости дополнительной кодировки/декодировки. В Python 2 существует разделение между str (байтовыми строками) и unicode (строками Юникода).

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


    • Вопрос №2: [ Что происходит в памяти, когда вы изменяете содержимое строки в Python? Почему строки в Python считаются неизменяемыми? ]

      Ответ
      • Что происходит в памяти:
        Когда вы "изменяете" строку в Python, вы на самом деле не изменяете существующую строку. Вместо этого создается новая строка, которая содержит результат изменений. Старая строка остается неизменной.
      • Почему строки в Python считаются неизменяемыми:
        Безопасность: Поскольку строки являются неизменяемыми, они не могут быть случайно изменены в разных частях программы, что уменьшает вероятность ошибок. Оптимизация: Неизменяемость позволяет Python кэшировать строки и использовать одну и ту же строку в разных местах памяти без необходимости создания новых копий. Это особенно полезно для маленьких и часто используемых строк. Использование в качестве ключей словаря: Неизменяемые объекты могут быть использованы в качестве ключей словаря. Это делает строки подходящими для этой роли, так как их содержимое не может быть изменено после создания. Семантическая ясность: Когда вы передаете строку в функцию или метод, вы можете быть уверены, что эта строка не будет изменена, что делает поведение программы предсказуемым.
      • Этот дизайнерский выбор в Python связан с многими преимуществами, но также требует осознания неизменяемости строк при написании кода, чтобы избежать ненужного создания большого числа временных строк и, как следствие, ненужных затрат памяти и процессорного времени.

    • Вопрос №3: [ Как эффективно соединить строки? ]

      Ответ
      • В Python существует несколько способов соединить строки. Некоторые из них более эффективны, особенно когда речь идет о больших объемах данных или множественных операциях конкатенации. Вот несколько популярных методов и их особенности:
        • Оператор +
          Этот метод прост и интуитивно понятен, но он может быть неэффективным при соединении большого количества строк из-за неизменяемости строк в Python.
        • Метод str.join()
          Этот метод часто рекомендуется для соединения больших списков строк, так как он более эффективен по сравнению с многократным использованием оператора +.
        • f-strings (доступны начиная с Python 3.6)
          f-strings предоставляют удобный способ вставки и форматирования строк, но они могут не подойти для действительно больших объемов данных.
        • Форматирование с помощью метода str.format()
          Этот метод более универсален и предлагает различные опции форматирования, но он может быть менее читаемым, чем f-strings.
        • Старый стиль форматирования с %
        • Этот стиль менее предпочтителен и считается устаревшим, но все еще встречается в старом коде.
      • Среди всех вышеперечисленных методов, если вам нужно соединить множество строк, наиболее эффективным является метод str.join().

    • Вопрос №4: [ Каким методом строки можно проверить, состоит ли строка только из буквенно-цифровых символов? ]

      Ответ
      • Для этой цели в Python есть встроенный метод str.isalnum(). Он возвращает True, если все символы в строке являются буквами или цифрами, и False в противном случае.

    • Вопрос №5: [ Как вы бы определили, является ли строка заголовком (начинается с большой буквы и далее идут только строчные буквы)? ]

      Ответ
      • Для этой цели можно использовать комбинацию встроенных методов строк. Сначала можно проверить, начинается ли строка с заглавной буквы с помощью str.istitle(), а затем проверить, содержат ли все остальные символы только строчные буквы.
        • Однако стоит отметить, что istitle() проверяет, чтобы каждое слово в строке начиналось с заглавной буквы
        • Если нужно удостовериться, что только первое слово начинается с заглавной буквы, а все остальные символы являются строчными буквами, то можно сделать так:
      s = "Hello"
      
      if s[0].isupper() and s[1:].islower():
          print("Строка является заголовком.")
      else:
          print("Строка не является заголовком.")

  • Численные типы
    • Вопрос №1: [ Какие численные типы ты знаешь? ]

      Ответ
      • В Python предоставляется несколько численных типов, предназначенных для различных задач математической обработки. Вот основные из них:
      • int:
        Представляет целые числа.
        В Python 2 существовали типы int (ограниченный размер) и long (произвольной длины), но в Python 3 они были объединены в один тип int с произвольной длиной.
      • float:
        Представляет числа с плавающей точкой (то есть действительные числа).
        Обычно реализован на основе двойной точности стандарта IEEE 754.
      • complex:
        Представляет комплексные числа.
        Имеет две части: действительную и мнимую, каждая из которых является числом с плавающей точкой.
        Пример: 3 + 4j.
      • bool:
        Представляет булевы значения: True и False.
        На самом деле является подтипом int, где True соответствует 1, а False соответствует 0.
      • Decimal (из модуля decimal):
        Представляет числа с плавающей точкой с фиксированной точностью.
        Используется тогда, когда требуется более высокая точность или определенное поведение округления, чем у обычных float.
      • Fraction (из модуля fractions):
        Представляет рациональное число как пару числитель/знаменатель.
        Позволяет выполнять арифметические операции с рациональными числами без потери точности.

    • Вопрос №2: [ Что такое комплексное число в Python и каким образом его можно определить? ]

      Ответ
      • Комплексные числа состоят из действительной и мнимой части. В Python комплексное число можно определить, используя суффикс j для мнимой части. Например: 3 + 4j

    • Вопрос №3: [ Каков максимальный размер int в Python? ]

      Ответ
      • В Python 3 int может динамически расти до пределов памяти вашего компьютера. Нет фиксированного максимального размера, как это было в Python 2 с int и long.

    • Вопрос №4: [ Каким образом в Python можно представить дробные числа с фиксированной точкой и зачем это может быть нужно? ]

      Ответ
      • Вы можете использовать модуль decimal, который предоставляет поддержку арифметики с фиксированной точкой. Это может быть полезно для финансовых вычислений, где точность имеет значение.

    • Вопрос №5: [ В чём разница между обычным делением (/) и делением нацело (//) ? ]

      Ответ
      • Обычное деление (/) возвращает результат в виде числа с плавающей точкой, в то время как деление нацело (//) возвращает только целую часть результата, отбрасывая дробную.

    • Вопрос №6: [ Что такое двоичное, восьмеричное и шестнадцатеричное представление чисел? Как их записать и преобразовать в Python? ]

      Ответ
      • Эти системы счисления представляют, соответственно, числа на основе 2, 8 и 16. В Python двоичное число можно записать, начав его с 0b, восьмеричное с 0o, и шестнадцатеричное с 0x. Например: 0b10, 0o10, 0x10. Для преобразования вы можете использовать встроенные функции: bin(), oct(), и hex().

    • Вопрос №7: [ Что такое плавающая точка и какие проблемы связаны с точностью при работе с типом float? ]

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

    • Вопрос №8: [ Какой результат выполнения 10 / 3 и почему? ]

      Ответ
      • Результат выполнения 10 / 3 равен 3.3333333333333335. В Python, деление двух целых чисел с использованием / возвращает число с плавающей точкой.

    • Вопрос №9: [ Как в Python можно выполнить побитовые операции с целыми числами? ]

      Ответ
      • В Python есть ряд побитовых операторов, таких как & (И), | (ИЛИ), ^ (Исключающее ИЛИ), ~ (НЕ), << (Сдвиг влево), и >> (Сдвиг вправо).

    • Вопрос №10: [ Что делает функция divmod() и каков её результат? ]

      Ответ
      • Ответ: Функция divmod(a, b) возвращает кортеж из частного и остатка от деления a на b.

    • Вопрос №11: [ Что такое экспоненциальная запись числа и как записать число в таком формате? ]

      Ответ
      • Экспоненциальная (или научная) запись числа - это способ записи числа в виде произведения числа (мантиссы) и 10, возведенного в степень. Например, 2e3 в Python представляет число 2 × 10^3 = 2000

    • Вопрос №12: [ Что происходит при делении на ноль для типов int и float? ]

      Ответ
      • При делении на ноль для типа int в Python будет выброшено исключение ZeroDivisionError. Для типа float, результатом деления на ноль будет бесконечность (inf) или отрицательная бесконечность (-inf), в зависимости от знака числителя.

  • Списки (list)
    • Вопрос №1: [ Что такое список (list)? ]

      Ответ
      • В Python список (list) — это упорядоченная изменяемая коллекция объектов. Это одна из самых универсальных и часто используемых структур данных в Python.
      • Основные особенности и понятия, связанные со списками:

      • Создание списков:
        Списки создаются, заключая элементы в квадратные скобки: [1, 2, 3].
        Элементы списка могут быть разных типов: [1, "строка", 3.14, [a, b, c]].

      • Индексация:
        Элементы списка индексируются, начиная с 0.
        Отрицательные индексы используются для индексации с конца списка: -1 указывает на последний элемент, -2 — на предпоследний и т. д.

      • Срезы:
        Срезы позволяют получать подсписки: a[1:3] возвращает список с элементами a[1] и a[2].
        Можно опустить начало или конец среза: a[:3] вернет первые три элемента, a[2:] вернет все элементы, начиная с третьего.

      • Изменяемость:
        Списки являются изменяемыми, что означает, что вы можете изменять, добавлять или удалять элементы после создания списка.

      • Встроенные методы:
        Списки предоставляют множество полезных методов, таких как append(), extend(), insert(), remove(), pop(), reverse(), и sort().

      • Вложенные списки:
        Списки могут содержать другие списки, создавая таким образом структуру, аналогичную двумерному массиву (или даже многомерному).

      • Длина списка:
        Функция len() позволяет получить количество элементов в списке.

      • Прохождение по списку:
        Списки можно легко итерировать с помощью цикла for.

      • Принадлежность элемента:
        Оператор in позволяет проверить, содержится ли элемент в списке.

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

    • Вопрос №2: [ Как создать пустой список двумя разными способами? ]

      Ответ
      • [] и list()

    • Вопрос №3: [ Что такое индексация и как получить доступ к элементу списка по индексу? ]

      Ответ
      • Индексация в списках позволяет получить доступ к элементам по их порядковому номеру. Элементы списка индексируются начиная с 0. Для доступа к элементу используется следующий синтаксис: list_name[index].

    • Вопрос №4: [ Какие методы можно использовать для добавления и удаления элементов из списка? ]

      Ответ
      • Добавление: append(), extend(), insert()
      • Удаление: remove(), pop(), del

    • Вопрос №5: [ Что такое вложенные списки и как получить доступ к элементам вложенного списка? ]

      Ответ
      • Вложенные списки – это списки, содержащие другие списки как свои элементы. Доступ к элементам вложенного списка можно получить с помощью последовательной индексации: list_name[index1][index2].

    • Вопрос №6: [ Чем отличается список от кортежа? ]

      Ответ
      • Списки изменяемы, то есть вы можете добавлять, удалять или изменять элементы после создания списка. Кортежи же неизменяемы. Также списки обозначаются квадратными скобками [], а кортежи – круглыми ().

    • Вопрос №7: [ Как создать копию списка? Чем отличается поверхностное копирование от глубокого? ]

      Ответ
      • Поверхностная копия: copy_list = original_list.copy() или copy_list = original_list[:].
      • Глубокое копирование создается с помощью модуля copy и его функции deepcopy().
      • При поверхностном копировании создается новый список, но его элементы – это ссылки на те же объекты, что и в оригинале.
      • При глубоком копировании создаются копии всех объектов в оригинальном списке, включая объекты, на которые ссылаются элементы.

    • Вопрос №8: [ Что делает метод sort() и как он отличается от функции sorted()? ]

      Ответ
      • sort(): сортирует список на месте (изменяет исходный список) и не возвращает новый список.
      • sorted(): возвращает новый отсортированный список, не изменяя исходный.

    • Вопрос №9: [ Какова сложность операции добавления элемента в конец списка? В начало списка? Как эта сложность изменяется с ростом размера списка? ]

      Ответ
      • append() - O(1)
      • insert() - O(n)
      • С увеличением размера списка время, необходимое для сдвига всех элементов, также увеличится, что делает эту операцию менее эффективной для больших списков по сравнению с добавлением в конец.

  • Картежи (tuple)
    • Вопрос №1: [ Что такое картеж (tuple)? ]

      Ответ
      • В Python кортеж (tuple) — это упорядоченная неизменяемая коллекция объектов. Основное отличие кортежа от списка заключается в его неизменяемости: после создания кортежа вы не можете изменять, добавлять или удалять его элементы.
      • Основные особенности и понятия, связанные с кортежами:

      • Создание кортежей:
        Кортежи создаются, заключая элементы в круглые скобки: (1, 2, 3).
        Для создания кортежа из одного элемента необходима запятая: (3,).
        Кортежи могут быть созданы и без использования круглых скобок: a = 1, 2, 3.

      • Индексация:
        Элементы кортежа индексируются так же, как и элементы списка, начиная с 0.
        Отрицательные индексы используются для индексации с конца кортежа.

      • Срезы:
        Срезы позволяют получать подкортежи так же, как и у списков.

      • Неизменяемость:
        Несмотря на то что кортежи неизменяемы, они могут содержать изменяемые объекты, например, списки.

      • Операции:
        Кортежи поддерживают все стандартные операции над последовательностями, такие как конкатенация и повторение.

      • Вложенные кортежи:
        Кортежи могут содержать другие кортежи, создавая таким образом структуру, аналогичную двумерному массиву (или даже многомерному).

      • Длина кортежа:
        Функция len() позволяет получить количество элементов в кортеже.

      • Прохождение по кортежу:
        Кортежи можно легко итерировать с помощью цикла for.

      • Принадлежность элемента:
        Оператор in позволяет проверить, содержится ли элемент в кортеже.

      • Неизменяемость как преимущество:
        Неизменяемость кортежей делает их хешируемыми, поэтому кортежи могут использоваться в качестве ключей в словарях (при условии, что все их элементы также хешируемы).
        Кортежи часто используются в Python для группировки данных, которые логически связаны между собой. Например, точку в трехмерном пространстве можно представить как кортеж из
        трех чисел. Также кортежи полезны там, где нужна гарантия, что данные не будут изменены, например, в качестве ключей в словарях или для возврата нескольких значений из функции.


    • Вопрос №2: [ Как создать кортеж, содержащий только один элемент? ]

      Ответ
      • Для создания кортежа с одним элементом необходимо поставить запятую после этого элемента: t = (5,).

    • Вопрос №3: [ Объясните, почему кортежи являются неизменяемыми. Какие практические преимущества это дает? ]

      Ответ
      • Кортежи неизменяемы, чтобы обеспечивать стабильность данных. Это делает их идеальными для использования в качестве ключей в словарях или элементов множеств. Поскольку они неизменяемы, они также могут быть быстрее в некоторых операциях по сравнению со списками.

    • Вопрос №4: [ Могут ли кортежи содержать изменяемые объекты, такие как списки? ]

      Ответ
      • Да, кортежи могут содержать изменяемые объекты, такие как списки. Например: t = (1, 2, [3, 4]).

    • Вопрос №5: [ Как можно объединить два кортежа в один? ]

      Ответ
      • Два кортежа можно объединить с помощью оператора +: например, t1 + t2.

    • Вопрос №6: [ Что такое распаковка кортежей? ]

      Ответ
      • Распаковка кортежей — это присваивание значений кортежа отдельным переменным. Например, если t = (1, 2, 3), то a, b, c = t присвоит a=1, b=2 и c=3.

    • Вопрос №7: [ Чем отличается tuple от namedtuple? Какие преимущества дает использование namedtuple? ]

      Ответ
      • namedtuple — это подкласс стандартного Python кортежа (tuple). Основное отличие заключается в том, что в namedtuple "у" полям можно задать имена, что делает код более читаемым. Преимущество использования namedtuple заключается в возможности обращаться к значениям по имени, а не только по индексу. Это делает код яснее и уменьшает вероятность ошибок.

    • Вопрос №8: [ Можно ли изменить содержимое списка, который находится внутри кортежа? Если да, как это повлияет на идентичность кортежа? ]

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

  • Множества (set)
    • Вопрос №1: [ Что такое множество (set)? ]

      Ответ
      • В Python множество (set) — это неупорядоченная коллекция уникальных объектов. Множества поддерживают математические операции над множествами, такие как объединение, пересечение, разность.
      • Основные особенности и понятия, связанные с множествами:

      • Создание множеств:
        Множество можно создать с помощью фигурных скобок: {1, 2, 3}.
        Для создания пустого множества используйте функцию set(), так как {} создает пустой словарь.

      • Уникальность элементов:
        Множество автоматически удаляет дубликаты: {1, 1, 2, 3} будет преобразовано в {1, 2, 3}.

      • Неупорядоченность:
        Множества не поддерживают порядок элементов, поэтому не гарантируется, что при итерации элементы будут возвращаться в каком-либо конкретном порядке.

      • Изменяемость:
        Стандартные множества (set) являются изменяемыми, что означает, что вы можете добавлять и удалять элементы.
        Существует также неизменяемый вариант множества — frozenset.

      • Операции над множествами:
        Множества поддерживают стандартные математические операции над множествами, такие как объединение (union или |), пересечение (intersection или &), разность (difference или -) и симметричную разность (symmetric_difference или ^).

      • Встроенные методы:
        Множества предоставляют методы, такие как add(), remove(), discard(), clear(), pop(), update(), и многие другие.

      • Принадлежность элемента:
        Оператор in позволяет быстро проверить наличие элемента в множестве.

      • Неизменяемость как ключ в словаре:
        Так как стандартные множества изменяемы, они не могут быть использованы в качестве ключей в словарях. Однако frozenset может использоваться в этой роли.
        Множества в Python часто используются для быстрой проверки принадлежности элемента, удаления дубликатов из списков и выполнения математических операций над группами данных.


    • Вопрос №2: [ В чем отличие множества от других коллекций, таких как список или кортеж? ]

      Ответ
      • Множества не поддерживают дубликаты, они неупорядочены (то есть порядок элементов может отличаться от порядка добавления) и элементы в них не индексируются.

    • Вопрос №3: [ Можно ли хранить изменяемые объекты внутри множества? Почему? ]

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

    • Вопрос №4: [ ? ]

      Ответ

    • Вопрос №5: [ Каковы основные преимущества использования множеств? ]

      Ответ
      • Быстрый поиск элемента (за константное время), легкость удаления дубликатов и возможность выполнения математических операций, таких как объединение, пересечение и разность.

    • Вопрос №6: [ Что такое "frozenset" и в чем его отличие от обычного множества? ]

      Ответ
      • frozenset - это неизменяемый вариант множества. Это значит, что после его создания вы не можете добавлять или удалять элементы. Основное применение frozenset заключается в том, что его можно использовать как ключ в словаре или элемент в другом множестве, так как он хешируем.

    • Вопрос №7: [ В чем разница между методами remove() и discard() при работе с множествами? ]

      Ответ
      • Оба метода удаляют элемент из множества, но remove() вызывает ошибку, если элемент не найден, в то время как discard() просто ничего не делает, если элемент отсутствует.

    • Вопрос №8: [ В чем принципиальная разница между множествами и словарями в Python, учитывая, что обе структуры используют хеширование? ]

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

    • Вопрос №9: [ Какова пространственная сложность множества в Python? В каких ситуациях использование множества может быть неэффективно с точки зрения использования памяти? ]

      Ответ
      • Пространственная сложность множества в основном зависит от количества элементов в нем. Однако множества обычно занимают больше места, чем списки, так как они используют дополнительную память для хеширования. Если вы хотите просто хранить данные и не выполнять по ним быстрый поиск или другие операции, специфичные для множеств, то список может быть более экономичным вариантом по памяти.

    • Вопрос №10: [ Как изменяется производительность множества при увеличении количества элементов, особенно когда размер множества приближается к границам текущей емкости хеш-таблицы? ]

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

  • Словари (dict)
    • Вопрос №1: [ Что такое словарь (dict)? ]

      Ответ
      • В Python, словарь (dict) представляет собой неупорядоченную коллекцию пар ключ-значение. Основное назначение словаря — обеспечивать возможность быстрого доступа к значению по его ключу.
      • Основные особенности и понятия, связанные со словарями:

      • Создание словаря:
        Словарь создается с помощью фигурных скобок: {ключ1: значение1, ключ2: значение2}. Например: {"apple": "green", "banana": "yellow"}
        Также можно использовать функцию dict(): dict(apple="green", banana="yellow").

      • Ключи и значения:
        Ключами словаря могут быть любые неизменяемые типы данных: числа, строки, кортежи и т. д.
        Значениями могут быть объекты любого типа: числа, строки, списки, другие словари и т. д.

      • Доступ к элементам:
        Значение по ключу можно получить, используя квадратные скобки: dict[key].
        Метод get() позволяет получать значение по ключу и указывать значение по умолчанию в случае, если ключ отсутствует: dict.get(key, default_value).

      • Изменяемость:
        Словари являются изменяемыми. Вы можете добавлять, изменять и удалять пары ключ-значение.

      • Встроенные методы:
        Словари предоставляют ряд полезных методов, таких как keys(), values(), items(), update(), clear(), pop(), popitem(), setdefault() и другие.

      • Порядок элементов:
        Начиная с версии Python 3.7, словари сохраняют порядок добавления элементов, хотя до этой версии порядок не гарантировался.

      • Принадлежность ключа:
        Оператор in позволяет проверить, есть ли ключ в словаре: key in dict.

      • Итерация:
        Можно итерироваться по ключам, значениям или парам ключ-значение словаря с помощью методов keys(), values() и items() соответственно.
        Словари в Python используются очень часто, так как они предоставляют удобный и быстрый способ хранения и доступа к данным по ключу.
        Этот тип данных особенно полезен для представления структур данных вроде баз данных, JSON-объектов, конфигураций и многого другого.


    • Вопрос №2: [ Какие типы данных можно использовать в качестве ключей словаря? ]

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

    • Вопрос №3: [ Как получить значение по ключу из словаря? ]

      Ответ
      • Значение можно получить, обратившись к ключу: value = my_dict["key1"] или с помощью метода get: value = my_dict.get("key1").

    • Вопрос №4: [ В чем разница между методами dict.get(key) и dict[key] при доступе к элементам словаря? ]

      Ответ
      • dict[key] возвращает значение по ключу или вызывает исключение KeyError, если ключ не найден. dict.get(key) возвращает значение по ключу или None (или другое указанное значение по умолчанию), если ключ не найден, не вызывая исключения.

    • Вопрос №5: [ Что произойдет, если попытаться получить значение по ключу, которого нет в словаре? ]

      Ответ
      • Если обратиться к ключу напрямую (например, my_dict["missing_key"]), будет вызвано исключение KeyError. Однако, если использовать метод get, то вернется None (или другое указанное значение по умолчанию).

    • Вопрос №6: [ Как проверить, присутствует ли ключ в словаре? ]

      Ответ
      • Можно использовать оператор in: if "key1" in my_dict:.

    • Вопрос №7: [ Как словари реализованы внутри Python? ]

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

    • Вопрос №8: [ Что такое хеш-функция, и как она связана со словарями? ]

      Ответ
      • Хеш-функция преобразует объект в число (хеш), которое затем используется для определения позиции объекта в хеш-таблице. В словарях ключи хешируются для быстрого доступа к соответствующим значениям.

    • Вопрос №9: [ Как в Python реализовано устранение коллизий в хеш-таблице, используемой для словарей? ]

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

    • Вопрос №10: [ Какие альтернативные структуры данных можно использовать вместо словаря, и в каких случаях это может быть оправдано? ]

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

  • Булевы типы (bool)
    • Вопрос №1: [ Что такое булев тип (bool)? ]

      Ответ
      • В Python, булев тип (bool) — это основной тип данных, представляющий истину или ложь. У него есть два возможных значения: True (истина) и False (ложь).
      • Особенности булевого типа в Python:

      • Производные от int:
        В Python, булевы значения на самом деле являются подклассом целых чисел. Так, True соответствует 1, а False соответствует 0. Это позволяет выполнять арифметические операции с булевыми значениями.

      • Приведение к булевому типу:
        Вы можете использовать функцию bool() для приведения других типов данных к булевому типу. Многие значения интерпретируются как False, когда они "пусты": например, пустая строка, пустой список, число 0 и т. д. Все остальные значения интерпретируются как True.

      • Логические операции:
        Булевы значения часто используются в логических выражениях с операторами and, or и not.

      • Сравнения:
        Операции сравнения (==, !=, <, >, <=, >=) возвращают булевы значения.

      • Условные выражения:
        Булевы значения часто используются в условных выражениях, таких как if, elif и while.


  • NoneType (None)
    • Вопрос №1: [ Что такое NoneType (None)? ]

      Ответ
      • В Python, NoneType — это тип данных для специального значения None, которое часто используется для обозначения отсутствия значения или для указания на то, что ничего не произошло или не было найдено.
      • Основные особенности и применение NoneType:

      • Единственное значение:
        У NoneType есть только одно возможное значение — None.

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

      • Возвращение из функции:
        Если функция в Python не содержит явного оператора return или содержит оператор return без значения, то она возвращает None.

      • Сравнение:
        None часто используется в сравнениях, чтобы проверить, имеет ли переменная значение. Например: if variable is None: или if variable is not None:.

      • Синглтон:
        None реализован как синглтон, что означает, что существует только один экземпляр None в программе. По этой причине для сравнения с None обычно используется оператор is, а не ==.

      • Параметры по умолчанию:
        В определениях функций None часто используется в качестве значения по умолчанию для аргументов.


  • ООП в Python
    • Вопрос №1: [ Как реализовано ООП в Python? ]

      Ответ
      • Объектно-ориентированное программирование (ООП) в Python реализовано посредством классов и объектов, а также механизмов наследования, инкапсуляции и полиморфизма
      • Классы и объекты:
        • Классы: Классы создаются с использованием ключевого слова class. Класс — это шаблон или чертёж для создания объектов (экземпляров).
        • Объекты: Объект — это экземпляр класса. Объект может иметь атрибуты (переменные, связанные с объектом) и методы (функции, связанные с объектом).
      • Инкапсуляция:
        Инкапсуляция — это механизм сокрытия внутренних данных класса от внешнего доступа. В Python, инкапсуляция реализована через префикс _ для "защищённых" атрибутов или методов и __ для "приватных" атрибутов или методов. Однако стоит понимать, что полной инкапсуляции, как в некоторых других языках, в Python нет — это скорее соглашение.
      • Наследование:
        Наследование позволяет создавать новый класс на основе существующего класса. Это обеспечивает повторное использование кода и устанавливает отношение между родительским и дочерним классом.
      • Полиморфизм:
        Полиморфизм — это способность различных объектов использовать методы с одинаковым именем, но с разной реализацией.
      • В целом, ООП в Python реализовано довольно гибко и интуитивно понятно. Python предоставляет все необходимые инструменты для создания структурированных и масштабируемых объектно-ориентированных программ.

  • Сборщик мусора в Python
    • Вопрос №1: [ Что такое сборщик мусора и как он реализован? ]

      Ответ
      • Сборщик мусора (Garbage Collector, GC) в Python — это механизм автоматического освобождения памяти, которая была выделена для объектов, но более не используется программой. Сборщик мусора в Python реализован на основе двух ключевых механизмов: подсчёта ссылок и циклического сборщика мусора.
      • Подсчёт ссылок (Reference Counting):
        У каждого объекта в Python есть счётчик ссылок, который фиксирует количество ссылок на данный объект. Когда на объект создаётся ссылка, счётчик увеличивается на единицу. Когда ссылка удаляется или выходит из области видимости, счётчик уменьшается на единицу. Как только счётчик ссылок объекта становится равным нулю (т.е. нет ссылок на объект), память, выделенная под объект, автоматически освобождается.
      • Циклический сборщик мусора:
        Хотя механизм подсчёта ссылок эффективен в большинстве ситуаций, он не может справиться с циклическими ссылками (когда объекты ссылаются друг на друга, образуя цикл). Для обнаружения и устранения таких циклических ссылок в Python реализован дополнительный механизм — циклический сборщик мусора. Этот сборщик мусора разделен на три "поколения". Новые объекты начинают своё существование в первом поколении. Если объекты переживают процесс сборки мусора, они перемещаются в следующее поколение (вплоть до третьего). Периодические проверки первого поколения происходят чаще, чем проверки второго и третьего поколений. Когда сборщик мусора запускается, он ищет циклические ссылки и освобождает память от объектов, которые составляют эти циклы.
      • Сборщик мусора в Python включен по умолчанию, но разработчики могут управлять им с помощью модуля gc, который предоставляет функции для включения/отключения сборщика мусора, принудительного запуска процесса сборки мусора и другие утилиты.
      • Важно отметить, что хотя сборщик мусора автоматизирует управление памятью, разработчикам всё равно стоит быть внимательными и освобождать ресурсы (например, файлы или соединения с базой данных) явно при необходимости.

  • Обработка исключений
    • Вопрос №1: [ Что такое обработка исключений в Python и как она реализована? ]

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

      • try ... except:
        Блок try содержит код, который может вызвать исключение. Блок except содержит код, который будет выполнен, если в блоке try произойдет исключение.

      • else:
        Блок else выполняется в том случае, если блок try был успешно завершен без исключений.

      • finally:
        Блок finally выполняется всегда, независимо от того, произошло исключение или нет. Это место идеально подходит для кода, который должен быть выполнен в любом случае (например, закрытие файла).

      • raise:
        Вы можете явно вызвать исключение с помощью ключевого слова raise.

      • assert:
        С помощью assert можно установить проверку, которая вызовет исключение (тип AssertionError), если условие ложно.

      • Пользовательские исключения:
        Вы можете создать собственные исключения, наследуя их от базового класса Exception или других встроенных исключений.

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


Практические задачи Python
  • Списки
    • Вопрос №1: [ soon ] Static Badge

      Код
      print("hello world")
      Ответ
      • Пояснение:

      • Ответ:



JavaScript
  • Общие вопросы по языку JavaScript
    • Вопрос №1: [ Soon ]

      Ответ


Практические задачи JavaScript
  • Soon
    • Вопрос №1: [ Soon ] Static Badge

      Код
      Ответ
      • Пояснение:

      • Ответ:



Java
  • Общие вопросы по языку Java
    • Вопрос №1: [ Soon ]

      Ответ


Практические задачи Java
  • Soon
    • Вопрос №1: [ Soon ] Static Badge

      Код
      Ответ
      • Пояснение:

      • Ответ:



C#
  • Общие вопросы по языку C#
    • Вопрос №1: [ Soon ]

      Ответ


Практические задачи C#
  • Soon
    • Вопрос №1: [ Soon ] Static Badge

      Код
      Ответ
      • Пояснение:

      • Ответ:



C++
  • Общие вопросы по языку C++
    • Вопрос №1: [ Soon ]

      Ответ


Практические задачи C++
  • Soon
    • Вопрос №1: [ Soon ] Static Badge

      Код
      Ответ
      • Пояснение:

      • Ответ:



About

Backend Interview Questions 2023 - это обширный ресурс помогающий в подготовке к техническим собеседованиям на позицию Backend Developer

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published