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

Открытое собеседование на Middle Go-разработчика в Wildberries: код-ревью

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

В этой статье мы подробно разберем открытое собеседование на позицию Middle Go-разработчика в Wildberries, с акцентом на формат code review. Мы проанализируем вопросы, ответы кандидата и предложим развернутые правильные ответы на каждый из них. Это поможет как кандидатам подготовиться к подобным собеседованиям, так и интервьюерам оценить уровень знаний и компетенций соискателей.

Теоретическая часть

Вопрос 1: Что такое интерфейс в Go и как вы понимаете его концепцию?

Таймкод: 00:05:55

Ответ собеседника: Неполный. Кандидат верно определяет интерфейс как контракт и упоминает Dependency Inversion Principle (DIP), но не полностью раскрывает преимущества использования интерфейсов, особенно в контексте Go.

Правильный ответ: Интерфейс в Go — это мощный инструмент для создания гибкого и расширяемого кода. Концептуально, интерфейс определяет контракт, набор методов, которым должен соответствовать тип, чтобы считаться реализующим этот интерфейс. В Go интерфейсы реализуются неявно: тип реализует интерфейс, если он предоставляет все методы, объявленные в интерфейсе.

Преимущества использования интерфейсов в Go:

  • Dependency Inversion (Принцип инверсии зависимостей): Интерфейсы позволяют отделить абстракцию от реализации. Бизнес-логика зависит от интерфейсов, а не от конкретных реализаций, что делает код более гибким и устойчивым к изменениям. Это ключевой принцип SOLID.
  • Заменяемость реализаций: Вы можете легко заменить одну реализацию интерфейса на другую, не затрагивая бизнес-логику, при условии, что новая реализация соответствует контракту интерфейса. Это упрощает тестирование, разработку и поддержку кода. Например, можно легко заменить реальный репозиторий базы данных на моковый репозиторий для юнит-тестов.
  • Тестируемость: Интерфейсы позволяют легко мокировать зависимости при тестировании. Вы можете создать мок-реализацию интерфейса, которая имитирует поведение реальной зависимости, и протестировать бизнес-логику изолированно.
  • Расширяемость: Интерфейсы позволяют добавлять новые реализации функциональности без изменения существующего кода, который зависит от интерфейса.
  • Полиморфизм: Интерфейсы обеспечивают полиморфизм, позволяя использовать переменные интерфейсного типа для хранения значений разных конкретных типов, реализующих этот интерфейс.

В Go интерфейсы являются "пустыми" (empty interfaces), если они не содержат объявленных методов. interface{} может быть реализован любым типом в Go, что делает его универсальным типом.

Вопрос 2: Объясните, что выведет представленный Go-код и почему.

Таймкод: 00:09:31

Ответ собеседника: Правильный. Кандидат верно определил, что программа выведет "panic", так как попытка вызвать метод Write на nil интерфейсе приведет к ошибке.

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

Программа выведет panic: runtime error: invalid memory address or nil pointer dereference.

Разберем код детально:

func main() {
var rw io.ReadWriter // Объявляем переменную rw типа io.ReadWriter (интерфейс)

myRw := NewReadWriter() // Вызываем NewReadWriter(), которая возвращает *myRw (указатель на структуру)

rw = myRw // Присваиваем rw значение myRw. ВАЖНО: rw теперь интерфейсная переменная

if rw == nil { // Проверка rw на nil.
fmt.Println("rw is nil!")
return
}

rw.Write([]byte("test")) // Вызов метода Write через интерфейс rw
fmt.Println("done")
}

func NewReadWriter() *myRw {
return nil // Функция возвращает nil указатель на структуру myRw
}

type myRwInterface interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}

type myRw struct {
}

func (myRw) Read(p []byte) (n int, err error) {
return 0, nil
}

func (myRw) Write(p []byte) (n int, err error) {
return 0, nil
}

Почему возникает panic?

  1. Nil интерфейс vs. Nil значение реализации: Ключевой момент здесь — различие между nil интерфейсной переменной и интерфейсной переменной, содержащей nil значение конкретного типа. В нашем случае, rw объявлена как io.ReadWriter. Когда мы присваиваем rw = myRw, где myRw является nil указателем на myRw структуру, rw не становится nil интерфейсом.

  2. Структура интерфейса: Интерфейсная переменная в Go представляет собой структуру данных, состоящую из двух частей:

    • type: динамический тип, который реализует интерфейс (в нашем случае, *myRw).
    • value: значение данных этого типа.
  3. В нашем коде:

    • myRw := NewReadWriter() возвращает nil (nil указатель на myRw).
    • rw = myRw присваивает интерфейсной переменной rw:
      • type: *myRw (тип известен, это указатель на структуру myRw)
      • value: nil (значение указателя - nil)
  4. Проверка if rw == nil: Проверка if rw == nil на строке 13 не вернет true, потому что сама интерфейсная переменная rw не является nil. Nil является только значение внутри интерфейсной переменной. Интерфейсная переменная rw имеет тип (*myRw) и значение (nil), поэтому она не nil.

  5. Вызов метода rw.Write: На строке 17 происходит вызов метода Write через интерфейсную переменную rw. Go пытается вызвать метод Write на значении, хранящемся в rw. Так как значение внутри rw — это nil (нулевой указатель), происходит попытка разыменования нулевого указателя, что приводит к panic.

Как исправить?

Можно добавить проверку внутри методов Write и Read, чтобы избежать вызова на nil указателе:

func (m *myRw) Write(p []byte) (n int, err error) {
if m == nil {
return 0, fmt.Errorf("Write called on nil *myRw pointer")
}
return 0, nil
}

func main() {
var rw io.ReadWriter

myRw := NewReadWriter()
rw = myRw

if _, err := rw.Write([]byte("test")); err != nil {
fmt.Println(err)
return
}

fmt.Println("done")
}

Теперь, если rw хранит nil указатель, метод Write вернёт ошибку, а не вызовет панику.

Вопрос 3: Принцип подстановки Лисков (LSP) и интерфейсы в Go.

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

Ответ собеседника: Неполный. Кандидат приводит пример с животными, но немного запутывается в объяснении принципа LSP. Верное направление мысли есть, но четкости не хватает.

Правильный ответ: Принцип подстановки Лисков (LSP) гласит: "Функции, которые используют указатели или ссылки на базовые типы, должны иметь возможность использовать объекты производных типов, не зная об этом." В контексте Go и интерфейсов, это означает, что любая реализация интерфейса должна быть взаимозаменяемой без нарушения ожидаемого поведения кода, который использует этот интерфейс.

Пример нарушения LSP:

Рассмотрим интерфейс Collection с методами Add и Len. Предположим, у нас есть две реализации этого интерфейса: List и Queue.

  • List: Метод Add добавляет элемент в конец списка, метод Len возвращает количество элементов.
  • Queue: Метод Add добавляет элемент в конец очереди, метод Len также возвращает количество элементов.

Теперь, представим, что реализация Queue нарушает LSP. Например, метод Add в Queue может добавлять элемент не только в конец, но и в начало, в зависимости от какого-то внутреннего состояния, или метод Len может возвращать количество элементов, увеличенное на 2 при каждом вызове.

Если код, который ожидает Collection, использует List и предполагает, что Add всегда добавляет в конец, а Len возвращает точное количество элементов, то при подстановке Queue с нарушением LSP поведение кода станет непредсказуемым и может привести к ошибкам.

LSP и интерфейсы в Go:

Интерфейсы в Go помогают соблюдать LSP, поскольку они определяют контракт. Если все реализации интерфейса соответствуют этому контракту, то они взаимозаменяемы. Однако, Go не гарантирует соблюдение LSP автоматически. Разработчики должны следить за тем, чтобы реализации интерфейсов вели себя предсказуемо и соответствовали ожиданиям кода, использующего интерфейс.

Вопрос 4: Пустой интерфейс interface{} (any) и его применение.

Таймкод: 00:18:46

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

Правильный ответ: Пустой интерфейс interface{} (или any в Go 1.18+) является интерфейсом, который не объявляет никаких методов. Это означает, что любой тип в Go неявно реализует пустой интерфейс. any является псевдонимом для interface{} и введен для улучшения читаемости кода.

Применение any (interface{}):

  • Универсальные коллекции (до дженериков): До появления дженериков, any был основным способом создания универсальных коллекций, которые могли хранить значения любых типов. Например, слайс []any.
  • Функции, принимающие аргументы произвольных типов: Функции могут принимать аргументы типа any, если им необходимо обрабатывать данные различных типов. Пример: fmt.Println принимает ...any.
  • Обработка неизвестных данных (например, JSON): При работе с JSON или другими форматами данных, структура которых заранее неизвестна, any используется для представления значений произвольных типов.
  • Реализация обобщенных алгоритмов (до дженериков): any позволял реализовывать обобщенные алгоритмы, которые могли работать с разными типами данных.
  • Передача функций как параметры: Пустой интерфейс может принимать функции, так как функции в Go являются типами первого класса и могут быть присвоены переменным типа any.

Ограничения any (interface{}):

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

Рекомендации по использованию any (interface{}):

  • Ограниченное использование: Используйте any только тогда, когда это действительно необходимо, например, для универсальных коллекций до дженериков, функций, обрабатывающих произвольные типы, или при работе с динамическими данными.
  • Предпочтение дженерикам (в Go 1.18+): В Go 1.18 и выше, для создания универсальных коллекций и алгоритмов лучше использовать дженерики, которые обеспечивают строгую типизацию и более высокую производительность.
  • Тщательная проверка типов: При использовании type assertion, всегда проверяйте тип значения, чтобы избежать паники.

Вопрос 5: Разница между асинхронностью и параллельностью.

Таймкод: 00:22:22

Ответ собеседника: Неполный. Кандидат верно различает асинхронность и параллельность, но не полностью раскрывает их применение и возможности Go в этом контексте.

Правильный ответ: Асинхронность и параллельность — это разные, хотя и взаимосвязанные, концепции в программировании.

  • Асинхронность: Асинхронность — это способ организации кода, при котором выполнение задачи не блокирует основной поток выполнения. Задача выполняется в "фоновом режиме", а основной поток может продолжать выполнение других задач. Когда асинхронная задача завершается, результат возвращается (например, через колбэк, промис или канал). Асинхронность достигается в рамках одного потока выполнения за счет переключения контекста между задачами во время ожидания ввода-вывода (I/O-bound tasks).

  • Параллельность: Параллельность — это одновременное выполнение нескольких задач на разных потоках (или ядрах процессора). Параллельность используется для ускорения выполнения вычислительно-интенсивных задач (CPU-bound tasks) за счет распределения нагрузки между несколькими ядрами.

Применение:

  • Асинхронность (I/O-bound tasks):

    • Сетевые запросы (HTTP-сервер, клиенты).
    • Операции ввода-вывода (чтение/запись файлов, работа с базами данных).
    • GUI-приложения (неблокирующий интерфейс пользователя).
    • Пример в Go: go routines, channels, select.
  • Параллельность (CPU-bound tasks):

    • Математические вычисления (обработка массивов, матричные операции).
    • Обработка изображений и видео.
    • Криптография.
    • Пример в Go: go routines в сочетании с GOMAXPROCS для использования нескольких ядер CPU.

Go и асинхронность/параллельность:

Go отлично подходит как для асинхронного, так и для параллельного программирования благодаря горутинам (goroutines) и каналам (channels).

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

Go runtime scheduler эффективно управляет горутинами, распределяя их выполнение между доступными потоками операционной системы. GOMAXPROCS позволяет управлять количеством потоков ОС, используемых Go runtime, тем самым влияя на степень параллелизма.

Вопрос 6: Функция runtime.GOMAXPROCS(1) и ее влияние на параллелизм.

Таймкод: 00:26:28

Ответ собеседника: Неполный. Кандидат верно говорит, что GOMAXPROCS(1) ограничивает количество процессорных ядер, используемых для параллельного выполнения горутин, но есть неточности в понимании взаимосвязи процессоров, машин (потоков ОС) и горутин.

Правильный ответ: Функция runtime.GOMAXPROCS(n) устанавливает максимальное количество операционных системных потоков, которые могут одновременно выполнять код Go. Значение n (где n - целое число) указывает на количество P (процессоров) в Go runtime scheduler.

  • P (Processor): Абстракция, представляющая собой вычислительный ресурс, необходимый для выполнения горутин. P привязан к M (OS Thread) и имеет свою локальную очередь горутин G.
  • M (Machine): Операционная системный поток. M выполняет код горутин.
  • G (Goroutine): Легковесный поток выполнения, пользовательская горутина.

runtime.GOMAXPROCS(1) означает:

  • Go runtime scheduler будет использовать только один процессор P.
  • Только одна горутина может выполняться действительно параллельно в любой момент времени, даже если доступно несколько ядер CPU.
  • Другие горутины будут выполняться конкурентно на этом одном P, переключаясь между собой во время ожидания ввода-вывода или других блокирующих операций.

Влияние на количество потоков ОС:

runtime.GOMAXPROCS(1) не ограничивает количество потоков ОС (M), используемых Go runtime в целом. Go runtime может использовать больше одного потока ОС для различных задач, таких как сборка мусора, сетевые операции и т.д. Однако, GOMAXPROCS ограничивает количество потоков ОС, которые могут одновременно выполнять пользовательский код Go.

Для CPU-bound задач: runtime.GOMAXPROCS(1) существенно снижает параллелизм и может замедлить выполнение CPU-bound задач, так как только одно ядро CPU будет использоваться для параллельного выполнения горутин.

Для I/O-bound задач: runtime.GOMAXPROCS(1) может не сильно влиять на производительность I/O-bound задач, так как горутины в основном проводят время в ожидании ввода-вывода, а не в активном вычислении.

Вопрос 7: gRPC и его преимущества перед REST.

Таймкод: 00:31:00

Ответ собеседника: Неполный. Кандидат верно указывает на бинарный формат данных Protocol Buffers, скорость и протокол HTTP/2 как преимущества gRPC, но не раскрывает все аспекты и нюансы выбора между gRPC и REST.

Правильный ответ: gRPC (Remote Procedure Calls) — это современный фреймворк для создания распределенных приложений, разработанный Google. Он основан на Remote Procedure Call (RPC) парадигме.

Преимущества gRPC перед REST:

  • Производительность:
    • Protocol Buffers (protobuf): gRPC использует Protocol Buffers для сериализации и десериализации данных. Protobuf — это бинарный формат, который более компактный и быстрый, чем текстовые форматы, такие как JSON или XML, используемые в REST.
    • HTTP/2: gRPC использует HTTP/2 в качестве транспортного протокола. HTTP/2 обеспечивает мультиплексирование соединений (множество запросов и ответов в одном TCP-соединении), сжатие заголовков, push-уведомления и приоритезацию запросов, что повышает эффективность и снижает задержки по сравнению с HTTP/1.1, используемым в REST.
    • Строгая типизация: gRPC использует IDL (Interface Definition Language) — Protocol Buffers для определения сервисов и сообщений. Это обеспечивает строгую типизацию, что позволяет на этапе компиляции обнаруживать ошибки, связанные с несовместимостью интерфейсов.
    • Генерация кода: Protobuf компилятор автоматически генерирует код клиента и сервера на разных языках программирования (Go, Java, Python, C++, и др.). Это упрощает разработку кросс-языковых микросервисов.
    • Потоковая передача (Streaming): gRPC поддерживает потоковую передачу данных в обоих направлениях (клиент-сервер и сервер-клиент), что эффективно для задач, требующих непрерывного обмена данными, таких как потоковое видео или real-time приложения.

Когда выбирать gRPC:

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

Когда выбирать REST:

  • Публичные API и взаимодействие с фронтендом: REST API более просты для потребления веб-браузерами и публичными клиентами, благодаря распространенности HTTP и JSON. gRPC требует gRPC-web proxy для взаимодействия с браузерами.
  • Простота и гибкость: REST более гибкий и менее строгий в отношении контракта API. Его легче разрабатывать и изменять, особенно на ранних этапах проекта.
  • Кэширование и мониторинг: REST API лучше поддерживаются инфраструктурой кэширования и мониторинга HTTP.
  • Интеграция с существующими системами: Если существующая инфраструктура и клиенты ориентированы на REST, то переход на gRPC может быть затратным.

Вопрос 8: Преимущества HTTP/2 перед HTTP/1.1 для gRPC.

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

Ответ собеседника: Неполный. Кандидат упоминает безопасность, сжатие, мультиплексирование и приоритезацию, но не раскрывает подробно каждый аспект и не все ответы точны.

Правильный ответ: HTTP/2 предоставляет ряд преимуществ по сравнению с HTTP/1.1, которые делают его идеальным транспортным протоколом для gRPC:

  • Мультиплексирование соединений: HTTP/2 позволяет отправлять несколько запросов и ответов параллельно через одно TCP-соединение. В HTTP/1.1 каждый запрос/ответ требует отдельного соединения, что создает накладные расходы на установку и разрыв соединений. Мультиплексирование в HTTP/2 снижает задержки и повышает пропускную способность, особенно для gRPC, который часто отправляет множество мелких сообщений.
  • Сжатие заголовков (HPACK): HTTP/2 использует алгоритм сжатия заголовков HPACK, который уменьшает размер заголовков HTTP, отправляемых по сети. Заголовки HTTP часто повторяются в последовательных запросах, и HPACK эффективно сжимает повторяющиеся заголовки, снижая трафик и задержки. Это особенно важно для gRPC, где заголовки могут быть значительной частью трафика.
  • Бинарный протокол: HTTP/2 — это бинарный протокол, в отличие от текстового HTTP/1.1. Бинарный формат более эффективен для парсинга и обработки, что снижает нагрузку на процессор как на клиенте, так и на сервере.
  • Приоритизация потоков: HTTP/2 позволяет задавать приоритеты для различных потоков данных. Это позволяет серверу более эффективно распределять ресурсы и отдавать приоритет более важным запросам. В gRPC это можно использовать для приоритезации критичных RPC-вызовов.
  • Server Push: HTTP/2 Server Push позволяет серверу проактивно отправлять данные клиенту до того, как клиент явно запросит их. Хотя gRPC сам по себе не использует Server Push напрямую, он использует потоковую передачу, которая концептуально схожа и реализуется эффективно благодаря мультиплексированию HTTP/2.
  • Безопасность (HTTPS by default): Хотя HTTP/2 сам по себе не требует шифрования, большинство реализаций HTTP/2 используют TLS (HTTPS) по умолчанию. Это обеспечивает безопасность и конфиденциальность данных, передаваемых через gRPC.
  • CRAM атаки: HTTP/2 и его методы сжатия заголовков (HPACK) не предотвращают CRIME атаки. CRIME атака была связана с SPDY (предшественник HTTP/2) и SSL/TLS сжатием. HTTP/2 не использует SSL/TLS сжатие, но HPACK сам по себе может быть потенциальной целью для атак, хотя и менее уязвимой. Проблема CRIME связана с комбинацией сжатия и шифрования на уровне протоколов, а не с самим HTTP/2.

В совокупности, эти преимущества HTTP/2 делают gRPC более быстрым, эффективным и масштабируемым по сравнению с REST, особенно для микросервисных архитектур, требующих высокой производительности и низкой задержки.

package main

import (
"fmt"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"net"
"net/http"
"os"
)

func checkErr(err error, msg string) {
if err == nil {
return
}
fmt.Printf("ERROR: %s: %s\n", msg, err)
os.Exit(1)
}

func main() {
H2CServerUpgrade()
}

// H2CServerUpgrade server supports "H2C upgrade" and "H2C prior knowledge" along with
// standard HTTP/2 and HTTP/1.1 that golang natively supports.
func H2CServerUpgrade() {
h2s := &http2.Server{}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %v, http: %v", r.URL.Path, r.TLS == nil)
})

server := &http.Server{
Addr: "0.0.0.0:1010",
Handler: h2c.NewHandler(handler, h2s),
}

checkErr(http2.ConfigureServer(server, h2s), "during call to ConfigureServer()")

fmt.Printf("Listening [0.0.0.0:1010]...\n")
checkErr(server.ListenAndServe(), "while listening")
}

Вопрос 9: Безопасность HTTP-соединений (TLS/SSL).

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

Ответ собеседника: Неполный. Кандидат описывает TLS handshake, но не совсем точно и не полностью раскрывает все этапы и детали процесса.

Правильный ответ: Безопасность HTTP-соединений (HTTPS) обеспечивается протоколом TLS (Transport Layer Security), который исторически был известен как SSL (Secure Sockets Layer). TLS handshake — это процесс установления защищенного соединения между клиентом и сервером.

Основные этапы TLS Handshake:

  1. Client Hello: Клиент отправляет сообщение "Client Hello" серверу. Это сообщение включает:

    • Версию TLS, которую поддерживает клиент.
    • Список поддерживаемых клиентом cipher suites (наборов алгоритмов шифрования, аутентификации и хэширования).
    • Случайное число (client random), которое будет использовано для генерации ключей шифрования.
    • Session ID (опционально): Для повторного использования TLS-сессии.
  2. Server Hello: Сервер отвечает сообщением "Server Hello", которое включает:

    • Выбранную сервером версию TLS.
    • Выбранный сервером cipher suite из предложенных клиентом.
    • Случайное число (server random).
    • Session ID (опционально): Если сервер решил возобновить сессию.
  3. Certificate: Сервер отправляет клиенту свой цифровой сертификат.

    Сертификат содержит:

    • Публичный ключ сервера.
    • Информация об организации, которой принадлежит сертификат.
    • Срок действия сертификата.
    • Подпись удостоверяющего центра (CA), подтверждающая подлинность сертификата.
  4. Server Key Exchange (зависит от cipher suite): В зависимости от выбранного cipher suite, сервер может отправить сообщение "Server Key Exchange", содержащее дополнительную информацию для обмена ключами (например, параметры Диффи-Хеллмана).

  5. Certificate Request (опционально): Если сервер требует аутентификацию клиента, он может отправить сообщение "Certificate Request", запрашивая клиентский сертификат.

  6. Server Hello Done: Сервер отправляет сообщение "Server Hello Done", сигнализируя о завершении этапа "Server Hello".

  7. Certificate (опционально, если сервер запросил): Если сервер запросил клиентский сертификат, клиент отправляет сообщение "Certificate", содержащее свой сертификат.

  8. Client Key Exchange: Клиент генерирует pre-master secret, шифрует его публичным ключом сервера из сертификата и отправляет зашифрованный pre-master secret серверу в сообщении "Client Key Exchange". (Используется асимметричное шифрование, обычно RSA или ECDH).

  9. Change Cipher Spec: Клиент отправляет сообщение "Change Cipher Spec", сигнализируя серверу о переключении на использование согласованных алгоритмов шифрования и ключей.

  10. Finished (Client): Клиент отправляет сообщение "Finished", которое зашифровано с использованием согласованных ключей и алгоритмов. Это сообщение подтверждает, что handshake прошел успешно и ключи обмена данными согласованы.

  11. Change Cipher Spec (Server): Сервер отправляет сообщение "Change Cipher Spec" клиенту.

  12. Finished (Server): Сервер отправляет сообщение "Finished" клиенту, также зашифрованное.

После успешного TLS Handshake:

  • Клиент и сервер имеют общие сессионные ключи (session keys), которые были сгенерированы на основе client random, server random и pre-master secret.
  • Все последующие данные между клиентом и сервером шифруются симметричными алгоритмами шифрования, которые были согласованы в cipher suite.
  • Аутентификация сервера подтверждена проверкой сертификата клиента. Клиентская аутентификация (если запрошена) подтверждается проверкой клиентского сертификата.

Роль центра сертификации (CA):

Центры сертификации (Certificate Authorities) играют ключевую роль в TLS/SSL, подтверждая подлинность цифровых сертификатов. Когда клиент получает сертификат от сервера, он проверяет:

  1. Подпись CA: Сертификат должен быть подписан доверенным CA. Клиент имеет список доверенных корневых сертификатов CA (обычно предустановленных в операционной системе или браузере). Клиент проверяет, что подпись сертификата сервера соответствует одному из доверенных корневых сертификатов CA.
  2. Цепочка доверия: Если сертификат сервера подписан не корневым CA, а промежуточным CA, клиент строит цепочку доверия, проверяя подпись промежуточного CA корневым CA и так далее, пока не достигнет доверенного корневого сертификата.
  3. Срок действия сертификата: Клиент проверяет, что срок действия сертификата не истек.
  4. Отзыв сертификата (CRL/OCSP): Клиент может проверить, не был ли отозван сертификат сервера, используя CRL (Certificate Revocation List) или OCSP (Online Certificate Status Protocol).

Вопрос 10: Разница между симметричным и асимметричным шифрованием.

Таймкод: 00:40:18

Ответ собеседника: Неполный. Кандидат имеет общее представление, но не дает четкого и полного объяснения различий и применения.

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

Симметричное шифрование:

  • Использует один и тот же ключ как для шифрования, так и для дешифрования данных.
  • Быстрое шифрование и дешифрование. Подходит для шифрования больших объемов данных.
  • Проблема обмена ключами: Ключ должен быть безопасно передан обеим сторонам (отправителю и получателю). Безопасная передача ключа является сложной задачей, особенно через незащищенные каналы связи.
  • Примеры алгоритмов: AES, DES, 3DES, RC4, ChaCha20.

Асимметричное шифрование (публичный ключ):

  • Использует пару ключей: публичный ключ и приватный ключ.
  • Публичный ключ: Может быть свободно распространен и использоваться для шифрования данных или проверки подписи.
  • Приватный ключ: Должен храниться в секрете и используется для дешифрования данных, зашифрованных публичным ключом, или для создания цифровой подписи.
  • Медленнее, чем симметричное шифрование. Обычно используется для шифрования небольших объемов данных, таких как ключи сессии, или для цифровой подписи.
  • Упрощает обмен ключами: Публичный ключ можно свободно распространять, и только владелец приватного ключа может дешифровать данные или создать валидную подпись.
  • Примеры алгоритмов: RSA, DSA, ECDSA, Diffie-Hellman, ElGamal.

Основные отличия в таблице:

ХарактеристикаСимметричное шифрованиеАсимметричное шифрование
Количество ключейОдинДва (публичный и приватный)
Ключ для шифрования/дешифрованияОдин и тот же ключРазные ключи
Скорость шифрования/дешифрованияВысокаяНизкая
Обмен ключамиСложный, небезопасныйПростой, безопасный
Основное применениеШифрование больших данныхОбмен ключами, цифровая подпись

Комбинированное использование (гибридная криптосистема):

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

  1. Асимметричное шифрование используется для безопасного обмена симметричным ключом. Клиент генерирует симметричный ключ сессии, шифрует его публичным ключом сервера и отправляет серверу.
  2. Симметричное шифрование используется для шифрования и дешифрования основной части данных. После обмена ключами, для шифрования и дешифрования больших объемов данных используется быстрый симметричный алгоритм, используя сессионный ключ.

TLS/SSL handshake использует именно такой гибридный подход для установления безопасного соединения.

Практическая часть: Code Review

Задача: Провести code review реализации сервиса Go, написанного Junior-разработчиком. Сервис запрашивает данные пользователя, генерирует PDF-документ и обновляет статус пользователя в базе данных.

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

В ходе code review собеседник (кандидат) выявил ряд проблем и предложил улучшения, которые можно сгруппировать по следующим категориям:

  • Транзакции:
    • Некорректное использование транзакций PostgreSQL на уровне бизнес-логики. Рекомендовано перенести управление транзакциями на уровень репозитория/адаптера БД.
    • Транзакция начинается до выполнения длительных операций (генерации PDF и запроса к внешнему сервису). Рекомендовано перенести начало транзакции ближе к операциям с БД, чтобы минимизировать время удержания блокировки и снизить вероятность конфликтов.
    • Отсутствует Rollback в случае ошибок. Необходимо добавить Rollback для обеспечения целостности данных при ошибках.
  • Наименование переменных и констант:
    • Неинформативное название константы SetForUs. Рекомендовано дать более осмысленное имя, отражающее ее назначение.
    • Утечка деталей реализации БД (названия колонок) в бизнес-логику. Рекомендовано абстрагироваться от деталей хранения данных на уровне репозитория.
  • Обработка ошибок:
    • Отсутствие обработки ошибок во многих местах (особенно при взаимодействии с внешними сервисами и БД). Рекомендовано добавить проверку и обработку ошибок, возвращая ошибки наверх для корректной обработки на уровне HTTP-handler.
    • Использование panic для обработки ошибок, что не является идиоматичным подходом в Go для бизнес-логики. Рекомендовано использовать возвращение ошибок (error).
  • Асинхронность и производительность:
    • Выполнение длительных операций (генерации PDF, внешних запросов) в рамках HTTP-handler, что может блокировать обработку других запросов и увеличивать время ответа. Рекомендовано перенести длительные операции в фоновые задачи (воркеры, очереди сообщений).
    • Запрос к внешнему сервису и генерация PDF выполняются синхронно в рамках транзакции. Рекомендовано выполнять эти операции асинхронно и вне транзакции для повышения производительности и снижения времени ответа.
  • Кэширование:
    • Для внешнего сервиса, который редко обновляет данные, целесообразно внедрить кэширование для снижения нагрузки и ускорения доступа к данным.
  • Архитектура:
    • Смешение ответственности между бизнес-логикой, репозиторием и фреймворком (pgx.Conn в бизнес-логике). Рекомендовано разделить слои приложения, придерживаясь принципов чистой архитектуры и Dependency Inversion.
    • Отсутствие интерфейсов для зависимостей (PDF-сервис, внешний сервис, репозиторий), что затрудняет тестирование и замену реализаций. Рекомендовано использовать интерфейсы для обеспечения гибкости и тестируемости.
  • Надежность и масштабируемость:
    • Отсутствие механизмов для обработки ошибок при взаимодействии с внешними сервисами (circuit breaker, retry). Рекомендовано внедрить эти механизмы для повышения надежности.
    • Для асинхронной генерации PDF предложено использовать очередь сообщений (Kafka, RabbitMQ) для обработки запросов в фоне и обеспечения масштабируемости.
    • Обновление кэша предложено реализовать через воркеры, работающие по расписанию, что требует учета проблем консистентности и race conditions.
  • Хранение PDF:
    • Сохранение PDF в базе данных не оптимально. Рекомендовано использовать блочные хранилища (S3) или другие object storage для хранения PDF-файлов.
    • При переходе на S3 возникает вопрос об откате операции сохранения PDF в S3 в случае ошибки, так как транзакции БД не распространяются на S3. Предложено использовать eventual consistency или компенсационные транзакции для обеспечения целостности данных.

Общая оценка Code Review:

Кандидат продемонстрировал уверенное владение Go на уровне Middle-разработчика, проявив глубокое понимание принципов разработки, архитектурных паттернов и проблем производительности и надежности. Code review проведен качественно и детально, с выявлением основных проблем и предложением конструктивных решений.

Заключение

Собеседование продемонстрировало как сильные, так и слабые стороны кандидата. Сильные стороны — практический опыт, понимание архитектурных принципов и внимание к деталям. Слабые стороны — недостаточная глубина в некоторых теоретических областях (сети, криптография, GMP). В целом, кандидат показал себя как Middle Go-разработчик, готовый к решению сложных задач и code review был пройден успешно.

Надеемся, этот подробный анализ был полезен для вас. Следите за нашими новыми публикациями и открытыми собеседованиями!