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

Вышвырнули За AI-код — Собеседуюсь На Его Место | Реальный Go-собес

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

Сегодня мы разберём реальное собеседование на позицию Go-разработчика в телеком-компанию, где кандидат с многолетним опытом работы на C++, Python и Java рассказывает о своём переходе на Go, оптимизации баз данных, распиливании монолитов и создании бойлерплейтов для ускорения разработки. В ходе интервью он демонстрирует типичные приёмы самопрезентации — от «скручивания» опыта до умелого обесценивания своих достижений с целью не показаться overqualified, а финал собеседования оказывается неожиданным: технически кандидат прошёл на ура, но не был принят из-за конфликта интересов, выявленного службой безопасности.

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

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

Ответ собеседника: Правильный. Коммерческий опыт начинается с 2015 года с C++. До этого был некоммерческий опыт с веб-разработкой на PHP, MySQL, jQuery. В университете программировал микроконтроллеры STM32 на чистом C, проектировал и паял печатные платы. Первое коммерческое место — разработка высокопроизводительных веб-сервисов на C++ с использованием Boost ASIO. Затем перешёл на Go, так как C++ проекты были в основном монолитами с устаревшим кодом. Выбор Go обусловлен ориентацией на высокую производительность и наличием низкоуровневых возможностей (указатели). Перешёл на Go без понижения грейда — остался мидл-разработчиком.

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

Ответ собеседника является хорошим примером структурированного рассказа о профессиональном опыте. Он демонстрирует прогрессию от низкоуровневой разработки (C, микроконтроллеры) к высокоуровневым веб-технологиям (C++, Go), что показывает широкий кругозор и понимание различных уровней абстракции.

Ключевые моменты, которые стоит отметить в таком ответе:

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

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

3. Демонстрация глубины знаний. Упоминание Boost ASIO для высокопроизводительных веб-сервисов на C++ показывает опыт работы с сетевым программированием и асинхронным вводом-выводом.

4. Практический опыт с железом. Опыт работы с микроконтроллерами STM32 и проектированием печатных плат добавляет ценности, так как показывает понимание аппаратного уровня.

Что можно было бы добавить для усиления ответа:

  • Конкретные проекты или задачи, которые решались на Go
  • Опыт работы с базами данных, очередями сообщений, контейнеризацией
  • Знание паттернов проектирования и архитектурных подходов
  • Опыт работы в команде, code review, менторство

Пример дополненного ответа:

«Мой коммерческий опыт начинается с 2015 года. До этого был некоммерческий опыт с веб-разработкой на PHP, MySQL, jQuery. В университете программировал микроконтроллеры STM32 на чистом C, проектировал и паял печатные платы — это дало понимание работы на низком уровне.

Первое коммерческое место — разработка высокопроизводительных веб-сервисов на C++ с использованием Boost ASIO. Работал с сетевым программированием, асинхронным вводом-выводом, многопоточностью.

Затем перешёл на Go. Выбор обусловлен ориентацией на высокую производительность, наличием низкоуровневых возможностей (указатели), а также простотой и скоростью разработки по сравнению с C++. На Go разрабатывал микросервисы, работал с PostgreSQL, Redis, Kafka, Docker, Kubernetes. Использовал паттерны: dependency injection, graceful shutdown, circuit breaker.

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

Вопрос 2. Какие задачи выполняли на Go и как проходил переход с C++?

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

Ответ собеседника: Правильный. На Go выполнялись те же CRUD-операции, работа с БД и Kafka, что и на C++. Переход был постепенным, без понижения грейда — остался мидл-разработчиком. Задачи начали выполняться с той же скоростью примерно на второй месяц. В следующей компании занялся более сложными задачами: оптимизация кода, кодогенерация, написание бойлерплейтов.

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

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

Типичные задачи на Go при переходе с C++:

1. CRUD-операции и работа с БД. Это базовые задачи, которые позволяют освоить экосистему Go. В Go для работы с БД обычно используют:

  • database/sql — стандартная библиотека
  • sqlx — расширение для удобной работы со структурами
  • ORM: gorm, ent, sqlboiler

2. Работа с очередями сообщений (Kafka). Популярные библиотеки:

  • sarama — низкоуровневая библиотека для Kafka
  • confluent-kafka-go — обёртка над librdkafka
  • segmentio/kafka-go — более высокоуровневая библиотека

3. Микросервисная архитектура. Типичные задачи:

  • Написание HTTP/gRPC сервисов
  • Работа с конфигурацией (Viper)
  • Логирование (zap, logrus)
  • Метрики (Prometheus)

Особенности перехода с C++ на Go:

Что проще в Go:

  • Отсутствие ручного управления памятью (сборщик мусора)
  • Простая модель многопоточности (горутины, каналы)
  • Быстрая компиляция
  • Встроенные инструменты тестирования и профилирования

Что сложнее при переходе:

  • Отсутствие исключений (явная обработка ошибок)
  • Отсутствие дженериков (до Go 1.18)
  • Другой подход к ООП (интерфейсы вместо наследования)
  • Более строгие правила форматирования кода

Пример кода — типичный HTTP-сервис на Go:

package main

import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gorilla/mux"
)

type Server struct {
router *mux.Router
db *sql.DB
}

func NewServer(db *sql.DB) *Server {
s := &Server{
router: mux.NewRouter(),
db: db,
}
s.routes()
return s
}

func (s *Server) routes() {
s.router.HandleFunc("/api/users", s.handleGetUsers()).Methods("GET")
s.router.HandleFunc("/api/users", s.handleCreateUser()).Methods("POST")
}

func (s *Server) handleGetUsers() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()

users, err := s.getUsersFromDB(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
}

func (s *Server) Run(addr string) error {
srv := &http.Server{
Addr: addr,
Handler: s.router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}

// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()

return srv.ListenAndServe()
}

Сложные задачи для роста после перехода:

1. Оптимизация кода:

  • Профилирование с помощью pprof
  • Оптимизация аллокаций
  • Работа с sync.Pool
  • Использование unsafe для критичных участков

2. Кодогенерация:

  • Использование go generate
  • Написание собственных генераторов
  • Инструменты: stringer, mockgen, protoc

3. Бойлерплейты и шаблоны:

  • Создание внутренних библиотек
  • Стандартизация логирования, метрик, конфигурации
  • Шаблоны микросервисов

Типичные сроки адаптации:

  • 1-2 недели: Базовый синтаксис, простые задачи
  • 1-2 месяца: Полная продуктивность, понимание экосистемы
  • 3-6 месяцев: Глубокое понимание идиом Go, оптимизация кода
  • 6+ месяцев: Наставничество, архитектурные решения

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

Вопрос 3. Занимались ли вы архитектурой и какие архитектурные задачи решали?

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

Ответ собеседника: Правильный. Распиливал монолиты на микросервисы. Использовал микросервисные паттерны: ретрайвы, API Gateway (в том числе умные, развёрнутые как отдельные Go-сервисы, которые редьюсили, мэтчили, джойнили JSON из разных сервисов). Работал с разделением запросов на чтение и запись. Решал задачи выбора между синхронным и асинхронным взаимодействием сервисов.

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

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

1. Распил монолита на микросервисы.

Это одна из самых распространённых архитектурных задач. Основные подходы:

Стратегии декомпозиции:

  • По бизнес-доменам (Domain-Driven Design) — наиболее предпочтительный подход
  • По техническим слоям — менее предпочтителен, может привести к распределённому монолиту
  • По частоте изменений — выделение часто меняющихся компонентов

Паттерн Strangler Fig (Душительная фига): Постепенная миграция функциональности из монолита в новые сервисы без остановки работы системы.

// Прирокси-сервиса, который перенаправляет запросы
type MigrationProxy struct {
oldService *LegacyClient
newService *NewServiceClient
config *MigrationConfig
}

func (p *MigrationProxy) HandleRequest(ctx context.Context, req *Request) (*Response, error) {
// Постепенное переключение трафика
if p.config.IsMigrated(req.Endpoint) {
return newService.Handle(ctx, req)
}
return oldService.Handle(ctx, req)
}

2. Микросервисные паттерны.

Ретрайы (Retries) с экспоненциальной задержкой:

package retry

import (
"context"
"math"
"time"
)

type Config struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
Multiplier float64
}

func Do(ctx context.Context, cfg Config, fn func() error) error {
var lastErr error

for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
if err := fn(); err != nil {
lastErr = err

// Экспоненциальная задержка с jitter
delay := time.Duration(math.Pow(cfg.Multiplier, float64(attempt))) * cfg.BaseDelay
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}

select {
case <-time.After(delay):
continue
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
return lastErr
}

Circuit Breaker (Автоматический выключатель):

package circuitbreaker

import (
"sync"
"time"
)

type State int

const (
StateClosed State = iota
StateOpen
StateHalfOpen
)

type CircuitBreaker struct {
mu sync.RWMutex
state State
failureCount int
failureThreshold int
timeout time.Duration
lastFailureTime time.Time
}

func New(failureThreshold int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
failureThreshold: failureThreshold,
timeout: timeout,
state: StateClosed,
}
}

func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()

switch cb.state {
case StateOpen:
if time.Since(cb.lastFailureTime) > cb.timeout {
cb.state = StateHalfOpen
} else {
cb.mu.Unlock()
return ErrCircuitOpen
}
}
cb.mu.Unlock()

err := fn()

cb.mu.Lock()
defer cb.mu.Unlock()

if err != nil {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.failureCount >= cb.failureThreshold {
cb.state = StateOpen
}
return err
}

// Успешный вызов
if cb.state == StateHalfOpen {
cb.state = StateClosed
}
cb.failureCount = 0
return nil
}

3. API Gateway как Go-сервис.

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

package gateway

import (
"context"
"encoding/json"
"net/http"
"sync"
)

type Gateway struct {
routes map[string]*RouteConfig
client *http.Client
}

type RouteConfig struct {
BackendServices []string
AggregationType AggregationType
}

type AggregationType int

const (
AggregationFirst AggregationType = iota
AggregationMerge
AggregationJoin
)

func (g *Gateway) HandleAggregatedRequest(w http.ResponseWriter, r *http.Request) {
route := g.matchRoute(r.URL.Path)

ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

// Параллельные запросы к бэкенд-сервисам
var wg sync.WaitGroup
results := make([]json.RawMessage, len(route.BackendServices))
errors := make([]error, len(route.BackendServices))

for i, service := range route.BackendServices {
wg.Add(1)
go func(idx int, svc string) {
defer wg.Done()
resp, err := g.callService(ctx, svc, r)
if err != nil {
errors[idx] = err
return
}
results[idx] = resp
}(i, service)
}

wg.Wait()

// Агрегация результатов
aggregated, err := g.aggregate(results, route.AggregationType)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(aggregated)
}

func (g *Gateway) aggregate(results []json.RawMessage, aggType AggregationType) (interface{}, error) {
switch aggType {
case AggregationMerge:
// Объединение JSON-объектов
merged := make(map[string]interface{})
for _, r := range results {
if r == nil {
continue
}
var m map[string]interface{}
if err := json.Unmarshal(r, &m); err != nil {
return nil, err
}
for k, v := range m {
merged[k] = v
}
}
return merged, nil

case AggregationJoin:
// Объединение массивов
var joined []interface{}
for _, r := range results {
if r == nil {
continue
}
var arr []interface{}
if err := json.Unmarshal(r, &arr); err != nil {
return nil, err
}
joined = append(joined, arr...)
}
return joined, nil

default:
return results[0], nil
}
}

4. CQRS (Command Query Responsibility Segregation).

Разделение запросов на чтение и запись.

package cqrs

// Command — операция записи
type CreateOrderCommand struct {
UserID string
Items []OrderItem
}

type CommandHandler interface {
Handle(ctx context.Context, cmd interface{}) error
}

type CreateOrderHandler struct {
eventStore EventStore
publisher EventPublisher
}

func (h *CreateOrderHandler) Handle(ctx context.Context, cmd interface{}) error {
createCmd, ok := cmd.(CreateOrderCommand)
if !ok {
return ErrInvalidCommand
}

order := NewOrder(createCmd.UserID, createCmd.Items)

// Сохранение событий
if err := h.eventStore.Save(ctx, order.ID, order.Events); err != nil {
return err
}

// Публикация событий для обновления read-модели
return h.publisher.Publish(ctx, order.Events)
}

// Query — операция чтения
type GetOrderQuery struct {
OrderID string
}

type QueryHandler interface {
Handle(ctx context.Context, query interface{}) (interface{}, error)
}

type GetOrderHandler struct {
readDB *sql.DB
}

func (h *GetOrderHandler) Handle(ctx context.Context, query interface{}) (interface{}, error) {
getQuery, ok := query.(GetOrderQuery)
if !ok {
return nil, ErrInvalidQuery
}

// Чтение из оптимизированной read-модели
return h.readDB.QueryRowContext(ctx,
"SELECT * FROM order_view WHERE id = $1", getQuery.OrderID)
}

5. Синхронное vs асинхронное взаимодействие.

Когда использовать синхронное (REST/gRPC):

  • Нужен немедленный ответ
  • Простые CRUD-операции
  • Строгая консистентность важна

Когда использовать асинхронное (Kafka/RabbitMQ):

  • Долгие операции
  • Событийная архитектура
  • Нужна высокая пропускная способность
  • Допустима eventual consistency
package messaging

// EventPublisher для асинхронного взаимодействия
type EventPublisher interface {
Publish(ctx context.Context, events []Event) error
}

type KafkaPublisher struct {
producer sarama.AsyncProducer
topic string
}

func (p *KafkaPublisher) Publish(ctx context.Context, events []Event) error {
for _, event := range events {
data, err := json.Marshal(event)
if err != nil {
return err
}

msg := &sarama.ProducerMessage{
Topic: p.topic,
Key: sarama.StringEncoder(event.AggregateID),
Value: sarama.ByteEncoder(data),
}

select {
case p.producer.Input() <- msg:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}

Ключевые принципы при выборе архитектуры:

  • Начинать с монолита, если нет явных причин для микросервисов
  • Декомпозировать по бизнес-доменам, а не по техническим слоям
  • Использовать асинхронное взаимодействие для развязки сервисов
  • Проектировать с учётом отказоустойчивости с самого начала
  • Мониторинг и наблюдаемость — обязательные компоненты

Вопрос 4. Какие ещё интересные задачи вы решали? Расскажите про работу с базой данных и кодогенерацию.

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

Ответ собеседника: Правильный. Реализовал Append Only модель (инкрементальную модель данных, где записи только добавляются, без обновлений — операция апдейта заменяется дублированием поля). Занимался кодогенерацией моков с конфигами. Реализовал кодогенерацию клиентского кода с использованием библиотек OpenAPI Tools (кроссплатформенная) и OAI (идиоматичная, написана на Go). Это позволило автоматически генерировать клиентский код из OpenAPI спецификации. Создал бойлерплейт для быстрого создания новых микросервисов с готовой структурой: Docker-файл, GitLab CI, конфиги, шаблонные handler, repository, service, config, подключение к PostgreSQL через PGX, готовый конфиг для го-линтера и моков. По базам данных предпочитает сырые SQL-запросы через библиотеку PGX, а не ORM.

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

Ответ собеседника демонстрирует опыт решения разнообразных архитектурных и инфраструктурных задач.

1. Append Only модель данных.

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

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

  • Полный аудит изменений
  • Возможность восстановить состояние на любой момент времени
  • Упрощение конкурентного доступа (нет блокировок на запись)
  • Естественная интеграция с Event Sourcing

Пример реализации:

-- Таблица с Append Only моделью
CREATE TABLE user_events (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
event_type VARCHAR(50) NOT NULL,
event_data JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
sequence_number BIGINT NOT NULL
);

CREATE INDEX idx_user_events_user_id ON user_events(user_id, sequence_number);

-- Получение текущего состояния пользователя
CREATE VIEW user_current_state AS
SELECT DISTINCT ON (user_id)
user_id,
event_data,
created_at
FROM user_events
ORDER BY user_id, sequence_number DESC;
package appendonly

import (
"context"
"encoding/json"
"fmt"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)

type EventStore struct {
db *pgxpool.Pool
}

type Event struct {
ID int64 `json:"id"`
UserID string `json:"user_id"`
EventType string `json:"event_type"`
EventData json.RawMessage `json:"event_data"`
SequenceNumber int64 `json:"sequence_number"`
}

func (s *EventStore) Append(ctx context.Context, userID, eventType string, data interface{}) error {
eventData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshal event data: %w", err)
}

tx, err := s.db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)

// Получаем следующий sequence number
var seqNum int64
err = tx.QueryRow(ctx, `
SELECT COALESCE(MAX(sequence_number), 0) + 1
FROM user_events
WHERE user_id = $1
FOR UPDATE
`, userID).Scan(&seqNum)
if err != nil {
return err
}

// Вставляем событие
_, err = tx.Exec(ctx, `
INSERT INTO user_events (user_id, event_type, event_data, sequence_number)
VALUES ($1, $2, $3, $4)
`, userID, eventType, eventData, seqNum)
if err != nil {
return err
}

return tx.Commit(ctx)
}

func (s *EventStore) GetCurrentState(ctx context.Context, userID string) (*UserState, error) {
var eventData []byte
err := s.db.QueryRow(ctx, `
SELECT event_data
FROM user_events
WHERE user_id = $1
ORDER BY sequence_number DESC
LIMIT 1
`, userID).Scan(&eventData)

if err != nil {
return nil, err
}

var state UserState
if err := json.Unmarshal(eventData, &state); err != nil {
return nil, err
}

return &state, nil
}

2. Кодогенерация моков.

//go:generate mockgen -source=interfaces.go -destination=mocks/mock_service.go -package=mocks

// Или используя mockery
//go:generate mockery --name=UserService --output=./mocks --outpkg=mocks

Конфигурация для mockery (mockery.yaml):

with-expecter: true
packages:
github.com/example/project/internal/service:
config:
all: true
dir: "internal/mocks"
filename: "mock_{{.InterfaceName}}.go"
mockname: "Mock{{.InterfaceName}}"
outpkg: "mocks"

3. Кодогенерация из OpenAPI.

# Используя oapi-codegen (идиоматичный Go)
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest

# Генерация типов
oapi-codegen -generate types -o types.gen.go -package api api.yaml

# Генерация сервера
oapi-codegen -generate chi-server -o server.gen.go -package api api.yaml

# Генерация клиента
oapi-codegen -generate client -o client.gen.go -package api api.yaml

Пример конфигурации oapi-codegen (cfg.yaml):

package: api
generate:
chi-server: true
client: true
models: true
embedded-spec: true
output: api.gen.go

4. Бойлерплейт микросервиса.

Типичная структура проекта:

service-name/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ └── handler.go
│ ├── service/
│ │ └── service.go
│ ├── repository/
│ │ └── repository.go
│ └── model/
│ └── model.go
├── migrations/
│ └── 001_init.sql
├── Dockerfile
├── .gitlab-ci.yml
├── .golangci.yml
├── go.mod
└── Makefile

Пример Dockerfile:

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY ../blog-draft .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server

# Runtime stage
FROM alpine:3.18
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /server .
COPY migrations/ ./migrations/
EXPOSE 8080
CMD ["./server"]

Пример .golangci.yml:

run:
timeout: 5m
go: "1.21"

linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam

linters-settings:
govet:
check-shadowing: true
gofmt:
simplify: true

5. Работа с PostgreSQL через PGX.

PGX — это низкоуровневая библиотека для работы с PostgreSQL, которая предоставляет больше контроля, чем стандартный database/sql.

package repository

import (
"context"
"fmt"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/pgconn"
)

type UserRepository struct {
db *pgxpool.Pool
}

func NewUserRepository(db *pgxpool.Pool) *UserRepository {
return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, user *User) error {
_, err := r.db.Exec(ctx, `
INSERT INTO users (id, email, name, created_at)
VALUES ($1, $2, $3, $4)
`, user.ID, user.Email, user.Name, user.CreatedAt)

if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return ErrDuplicateEmail
}
return fmt.Errorf("insert user: %w", err)
}
return nil
}

func (r *UserRepository) GetByID(ctx context.Context, id string) (*User, error) {
var user User
err := r.db.QueryRow(ctx, `
SELECT id, email, name, created_at, updated_at
FROM users
WHERE id = $1
`, id).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.CreatedAt,
&user.UpdatedAt,
)

if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("get user: %w", err)
}
return &user, nil
}

// Использование транзакций
func (r *UserRepository) CreateWithProfile(ctx context.Context, user *User, profile *Profile) error {
tx, err := r.db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)

// Вставка пользователя
_, err = tx.Exec(ctx, `
INSERT INTO users (id, email, name, created_at)
VALUES ($1, $2, $3, $4)
`, user.ID, user.Email, user.Name, user.CreatedAt)
if err != nil {
return err
}

// Вставка профиля
_, err = tx.Exec(ctx, `
INSERT INTO profiles (user_id, bio, avatar_url)
VALUES ($1, $2, $3)
`, profile.UserID, profile.Bio, profile.AvatarURL)
if err != nil {
return err
}

return tx.Commit(ctx)
}

// Использование batch для массовых операций
func (r *UserRepository) BulkCreate(ctx context.Context, users []*User) error {
batch := &pgx.Batch{}

for _, user := range users {
batch.Queue(`
INSERT INTO users (id, email, name, created_at)
VALUES ($1, $2, $3, $4)
`, user.ID, user.Email, user.Name, user.CreatedAt)
}

br := r.db.SendBatch(ctx, batch)
defer br.Close()

for i := 0; i < len(users); i++ {
_, err := br.Exec()
if err != nil {
return fmt.Errorf("batch insert at index %d: %w", i, err)
}
}

return br.Close()
}

Преимущества PGX перед ORM:

  • Полный контроль над SQL-запросами
  • Лучшая производительность
  • Поддержка специфичных возможностей PostgreSQL (LISTEN/NOTIFY, COPY, массивы)
  • Меньше магии, код более предсказуем
  • Легче оптимизировать запросы

Когда использовать ORM:

  • Простые CRUD-операции
  • Быстрое прототипирование
  • Команда не имеет сильных SQL-навыков

Вопрос 5. Почему вы не прошли испытательный срок на предыдущем месте работы?

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

Ответ собеседника: Правильный. Не прошёл службу безопасности, потому что на его имя было открыто ИП/ООО. Компания увидела риск конкуренции, так как вид деятельности по ИП совпадал с деятельностью компании. Технический собес был пройден успешно, но отказ пришёл по результатам проверки СБ.

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

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

Как правильно отвечать на этот вопрос:

1. Будьте честны, но лаконичны. Не нужно углубляться в детали конфликтов или обвинять работодателя. Кратко опишите ситуацию и сделайте выводы.

2. Покажите, что вы извлекли урок. Интервьюер хочет понять, что вы не повторите подобную ситуацию.

3. Не обесценивайте свой опыт. Даже если испытательный срок не пройден, вы получили опыт и навыки.

Примеры хороших ответов:

Вариант 1 (как в данном случае): «Не прошёл проверку службы безопасности из-за наличия ИП с аналогичным видом деятельности. Техническую часть прошёл успешно. Сейчас ИП закрыто, подобных ситуаций больше не возникнет.»

Вариант 2 (если были другие причины): «Не сложились отношения с руководителем по стилю управления. Я предпочитаю автономную работу с чёткими требованиями, а ожидался более тесный контроль. Сейчас я лучше понимаю, какой формат работы мне подходит.»

Вариант 3 (если компания закрылась/сократила): «Компания провела сокращение штата на испытательном сроку. Это было не связано с моей работой — сократили весь отдел.»

Чего стоит избегать:

  • Подробных рассказов о конфликтах
  • Обвинений бывшего работодателя
  • Оправданий без конкретики
  • Излишнего негатива

Что интересует интервьюера:

  • Есть ли риск повторения ситуации
  • Как вы справляетесь с трудностями
  • Насколько вы ответственны и честны
  • Какие выводы сделали

Рекомендации для будущего:

  • Закрывайте ИП/ООО, если они могут вызвать конфликт интересов
  • Уточняйте требования службы безопасности до выхода на работу
  • Имейте готовое объяснение для подобных ситуаций
  • Сохраняйте хорошие отношения с бывшими коллегами — они могут быть рекомендателями