Как я завалил кучу собесов по Go и что из этого вынес

Страницы:  1

Ответить
 

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--Источник
 
Loading...
Error