|
Professor Seleznov
|
Привет! Меня зовут Александр. Некоторые могут помнить мои статьи про финансовую аналитику на Python - анализ ETF, оптимизацию портфелей. Но последние 6 лет я Senior Go Backend Engineer, специализируюсь на финтехе и трейдинге. Эта комбинация - domain expertise в финансах + техническая экспертиза в Go - оказалась очень ценной. Но путь был тернистым. Последние полгода активно собеседовался: 8 интервью в разных компаниях - от крупных российских IT-гигантов до международных финтех стартапов. Где-то взяли, где-то нет. Решил поделиться самыми дурацкими ошибками, которые делал я и другие кандидаты. Может, кому поможет не наступить на те же грабли. Немного предыстории В начале прошлого года решил поменять работу. Думаю - ну что там сложного, Go знаю, опыта хватает, пойду поумничаю на интервью. Ага, щас! Первый же собес в крупной компании завалил так, что до сих пор стыдно. Но обо всем по порядку. Вот топ-10 косяков, которые я либо сам делал, либо видел у других кандидатов. - 1. “Что выведет этот код?” - и тут началось… Боже, сколько раз я на этом прокалывался! Особенно запомнился интервью в одной крупной финтех компании (международная, специализируется на трейдинге). Дают мне код:
func main() { s := make([]int, 0, 5) s = append(s, 1, 2, 3) a := append(s, 4) b := append(s, 5) fmt.Println(a) fmt.Println(b) }
Я, умный такой, отвечаю: “Ну понятно же - a будет [1,2,3,4], а b будет [1,2,3,5]”. Интервьюер так грустно на меня посмотрел… Оказывается, оба слайса будут [1,2,3,5]! Почему? Да потому что underlying array у них общий, capacity хватает, и когда мы делаем append(s, 5), это затирает четверку в том же массиве. Я тогда честно признался: “Не, ну это я не знал”. Интервьюер говорит: “А как же вы баги в проде ловите?” А я думаю: “Ну… методом тыка?” Конечно, вслух не сказал, но по лицу было видно. Мораль: Надо знать, как слайсы устроены под капотом. Не просто “это динамический массив”, а реально понимать про capacity и underlying array. - 2. Race conditions - “Да что тут такого сложного?” Это вообще отдельная песня. В одной криптоплатежной компании (европейская, миллиарды в обороте) дали задачку написать простенький счетчик:
type Counter struct { value int } func (c *Counter) Increment() { c.value++ // Чего тут сложного-то? }
Я говорю: “Ну все, готово”. А интервьюер: “А что будет, если много горутин будут это вызывать?” И тут я понял, что проштрафился. c.value++ - это не одна операция, а три: прочитать, увеличить, записать. А между ними другая горутина может влезть. Правильно было бы так:
type Counter struct { mu sync.Mutex value int } func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.value++ }
Или еще проще - через atomic:
type Counter struct { value int64 } func (c *Counter) Increment() { atomic.AddInt64(&c.value, 1) }
Фишка в том, что надо понимать КОГДА что использовать. Для простого счетчика - atomic. Для сложной логики - mutex. Для передачи данных между горутинами - channels. Кстати, один интервьюер мне формулу дал: “atomic → RWMutex → Mutex → channel”. По возрастанию сложности и накладных расходов. - 3. PostgreSQL и деньги - тут шутки плохи А вот это был реально стыдный момент. В той же финтех компании дают задачу: “Напишите функцию перевода денег между счетами”. Я, наивный, пишу:
func TransferMoney(from, to int, amount decimal.Decimal) error { tx, err := db.Begin() // ... проверки ошибок var balance decimal.Decimal err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", from).Scan(&balance) if balance.LessThan(amount) { return errors.New("денег нет, но вы держитесь") } // Списываем tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from) // Зачисляем tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to) return tx.Commit() }
Интервьюер спрашивает: “А что будет, если две транзакции одновременно переводят деньги с одного счета?” Я думаю: “Ну, транзакция же, изоляция…” А он говорит: “А между SELECT и UPDATE другая транзакция может успеть?” И правда может! Классический Lost Update. Надо было блокировать строку:
err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ? FOR UPDATE", from).Scan(&balance)
Этот FOR UPDATE блокирует строку до конца транзакции. Урок: В финтехе шутки плохи. Не знаешь SELECT FOR UPDATE - не работай с деньгами. - 4. Context - “А зачем он мне?” Помню собес в одной большой российской IT-компании. Дают задачку написать функцию, которая ходит в API. Я пишу:
func GetUser(userID int) (*User, error) { resp, err := http.Get(fmt.Sprintf("http://api/users/%d", userID)) // дальше парсинг... }
“А context где?” - спрашивает интервьюер. “А зачем он тут? Это же просто GET запрос” - отвечаю я. Оказывается, зачем:
- Timeout control - а то запрос может висеть вечно
- Cancellation - если пользователь ушел, зачем тратить ресурсы?
- Trace propagation - для мониторинга
Правильно:
func GetUser(ctx context.Context, userID int) (*User, error) { req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://api/users/%d", userID), nil) client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) // ... }
Правило: Context - первый параметр в любой функции, которая может “повиснуть”. - 5. Горутины и утечки памяти Это я понял не сразу. На одном собесе дают задачу: “Обработайте массив данных параллельно”. Я, как настоящий гений, пишу:
func ProcessData(data []string) { for _, item := range data { go func(item string) { result := heavyProcessing(item) fmt.Println(result) // И кто это будет читать? }(item) } }
“А как вы узнаете, что все обработалось?” - спрашивает интервьюер. “Ну… подождем?” - говорю я. “Сколько?” “Ну… достаточно?” Конечно, это неправильно. Горутины запустятся и “уплывут”. В production это memory leak. Правильно - через WaitGroup:
func ProcessData(data []string) error { var wg sync.WaitGroup errCh := make(chan error, len(data)) for _, item := range data { wg.Add(1) go func(item string) { defer wg.Done() if err := heavyProcessing(item); err != nil { errCh <- err } }(item) } go func() { wg.Wait() close(errCh) }() for err := range errCh { if err != nil { return err } } return nil }
Фишка: Всегда знай, когда твои горутины закончатся. И как ловить их ошибки. - 6. interface{} - универсальное зло Ох, сколько я на этом налетел! Думал - раз Go такой строгий с типами, то interface{} - это способ “расслабиться”.
func Process(data interface{}) interface{} { switch v := data.(type) { case string: return strings.ToUpper(v) case int: return v * 2 case []interface{}: // тут начинается рекурсивный ад... default: return "хз что это" } }
Интервьюер говорит: “А что будет, если я передам map[string]int?” Я: “Ну… вернется ‘хз что это’” “А вы узнаете об этом когда?” “Ну… в runtime?” “Вот именно. А можно узнать раньше?” Оказывается, можно. Либо через generics (если Go 1.18+), либо просто нормальными типами:
type UserRequest struct { Name string `json:"name"` } func ProcessUser(req UserRequest) (*UserResponse, error) { // Тут все понятно и type-safe }
Правило: interface{} только когда РЕАЛЬНО нужен any type. А это редко. - 7. Ошибки - “Залогировал и забыл” Еще один мой косяк. Пишу функцию:
func GetUser(id int) *User { user, err := db.GetUser(id) if err != nil { log.Printf("Ошибка: %v", err) // "Залогировал же!" return nil } return user }
Интервьюер: “А как вызывающий код поймет, что произошло?” Я: “Ну… nil же вернется” “А nil может быть и потому что пользователя нет, и потому что БД упала?” Ага, точно. Caller не поймет, что делать с nil. Правильно:
func GetUser(id int) (*User, error) { user, err := db.GetUser(id) if err != nil { return nil, fmt.Errorf("не смог достать пользователя %d: %w", id, err) } return user, nil }
Фишка в%w - он сохраняет оригинальную ошибку, и ее потом можно распаковать через errors.Unwrap(). - 8. ClickHouse - “Это же просто SQL” Вот тут я реально не знал специфики. В одной финтех компании активно используют ClickHouse. Дают задачку написать запрос для аналитики:
SELECT user_id, COUNT(*) FROM events WHERE event_type = 'click' AND timestamp >= '2026-01-01' GROUP BY user_id ORDER BY COUNT(*) DESC
Я думаю - ну нормально же? А интервьюер морщится: “А по чему у вас ORDER BY в таблице?” Оказывается, в ClickHouse все таблицы отсортированы по ORDER BY ключу. А если фильтруешь не по нему - будет медленно. Правильно:
SELECT user_id, count() as clicks FROM events WHERE timestamp >= '2026-01-01' AND timestamp < '2026-02-01' -- Используем партиции AND event_type = 'click' -- После timestamp! GROUP BY user_id ORDER BY clicks DESC
Урок: Column-oriented БД - не PostgreSQL. У них свои заморочки. - 9. Kafka - “Читаю и обрабатываю” На собесе в одной high-load компании дали задачку с Kafka. Я написал что-то вроде:
for { msg, err := consumer.ReadMessage(-1) if err == nil { processMessage(msg.Value) // А если упадет? } }
“А что будет, если processMessage упадет с паникой?” - спрашивает интервьюер. “Ну… restart?” - говорю я. “А offset закоммитится?” И тут я понял, что не подумал про exactly-once delivery. Если message обработался, но offset не закоммитился - после рестарта сообщение придет снова. Правильно - коммитить offset только после успешной обработки:
for { msg, err := consumer.ReadMessage(100 * time.Millisecond) if err != nil { continue } if err := processMessage(msg.Value); err != nil { log.Printf("Не смог обработать: %v", err) continue // НЕ коммитим offset } consumer.CommitMessage(msg) // Коммитим только тут }
Урок: В distributed systems каждая мелочь важна. - 10. System Design - тут совсем грустно Самый сложный этап. В одной крупной российской IT-компании дали спроектировать URL Shortener. Я говорю: “Ну делаем REST API. POST /shorten принимает URL, возвращает ID. GET /:id возвращает original URL. В PostgreSQL храним mapping. Все!” Интервьюер: “А сколько URL в день вы ожидаете?” “Ну… много?” “Давайте цифры” И тут началось… Оказывается, надо считать:
- 100 млн URL в день = ~1200 writes/sec
- Read:Write = 100:1 = 120,000 reads/sec
- Хранение за 5 лет = терабайты
Один PostgreSQL не потянет. Нужен кеш (Redis), CDN для популярных ссылок, шардинг БД… Урок: System Design - это не “какую БД выбрать”, а понимание масштаба и trade-offs. - Неожиданный бонус: финансовый бэкграунд Кстати, мой путь analyst → Go developer оказался преимуществом! В финтех компаниях часто спрашивают не только про код, но и про бизнес-логику:
- “Как обеспечить exactly-once delivery для платежей?”
- “Что такое идемпотентность в контексте денежных переводов?”
- “Как бы вы реализовали matching engine?”
Когда я рассказываю про опыт с ETF анализом, portfolio optimization, понимание рынков - это сразу выделяет среди других кандидатов. Интервьюеры понимают, что я не просто пишу код, а понимаю зачем. Урок: Ваш нетехнический опыт может стать конкурентным преимуществом! Что я понял после всех этих собесов
- Дело не в синтаксисе Go - дело в том, как ты мыслишь как engineer
- Production опыт стоит больше, чем знание всех фишек языка
- Domain knowledge может быть решающим фактором
- Честность лучше чем попытка заболтать непонимание
- Вопросы обратно показывают, что ты думаешь о проблеме
Что реально спрашивают:
- 60% - concurrency и как оно работает под капотом
- 30% - databases и distributed systems
- 10% - алгоритмы (и то не leetcode, а практические)
Что помогло получить офферы:
- Рассказывать про реальные проблемы которые решал
- Объяснять trade-offs - почему выбрал именно это решение
- Признавать пробелы - “не знаю, но думаю так…”
- Задавать вопросы - уточнять требования, обсуждать альтернативы
- Что дальше: от статьи к практике Все эти материалы - результат месяцев подготовки и анализа реальных собеседований. Но это только верхушка айсберга. Запускаю Telegram канал @go_interview_prep_ru, где буду регулярно делиться: 📝 Подробными разборами задач с реальных собесов
🧠 Еженедельными Go quiz с объяснениями
💡 Инсайдами про процессы топовых компаний
🔧 Практическими советами для Senior уровня
💰 Данными по зарплатам и market trends Фокус канала - не теория из учебников, а реальные вопросы, которые задают на собесах прямо сейчас. Также готовлю полноценный курс по подготовке к Go интервью. В основе - реальный опыт, настоящие задачи, проверенные на практике подходы. 🎯 Для кого это будет полезно:
- Middle → Senior Go разработчики
- Backend engineers из других языков, переходящие на Go
- Финтех разработчики (особенно ценю domain expertise!)
- Career changers как я - никогда не поздно расти
Подписывайтесь: t.me/go_interview_prep_ru--Источник
|