Привет! Меня зовут Александр. Некоторые могут помнить мои статьи про финансовую аналитику на 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_idORDER BY COUNT(*) DESC
Я думаю — ну нормально же? А интервьюер морщится: “А по чему у вас ORDER BY в таблице?”
Оказывается, в ClickHouse все таблицы отсортированы по ORDER BY ключу. А если фильтруешь не по нему — будет медленно.
Правильно:
SELECT user_id, count() as clicksFROM events WHERE timestamp >= '2026-01-01' AND timestamp < '2026-02-01' -- Используем партиции AND event_type = 'click' -- После timestamp!GROUP BY user_idORDER 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
ссылка на оригинал статьи https://habr.com/ru/articles/1030108/