> Как реализовать коллекцию для хранения связи между customerId, accountNumber и статусом ареста счета без использования объектов? (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Альфа-банк

Стек: Go

> Пример ответа

Короткий ответ

Для хранения связи между customerId, accountNumber и статусом ареста без объектов в Go используем три параллельных среза (slice) или map с составным ключом. Оптимальный вариант - map[string]int, где ключ - конкатенация customerId и accountNumber через разделитель, значение - статус ареста. Это даёт O(1) доступ и не требует создания структур. Для batch-операций подойдут параллельные срезы с индексной синхронизацией.

Подробное объяснение

В Go отсутствуют объекты в классическом ООП-смысле, но есть структуры. Если задача - избежать структур, используем примитивные типы. Основные подходы:

  1. Map с составным ключом: map[string]int. Ключ формируется как fmt.Sprintf("%d:%s", customerId, accountNumber) или через strconv.Itoa(customerId) + ":" + accountNumber. Плюсы: быстрый доступ, простота. Минусы: конкатенация строк на каждую операцию, риск коллизий при неправильном разделителе.

  2. Параллельные срезы: customerIDs []int, accountNumbers []string, statuses []int. Индекс - неявная связь. Плюсы: компактность, кеш-локальность. Минусы: сложность удаления, риск рассинхронизации.

  3. Вложенные map: map[int]map[string]int - customerId → accountNumber → статус. Плюсы: естественная группировка по customerId. Минусы: избыточность памяти, сложность обновления.

  4. Битовые маски или массивы фиксированной длины - если домен значений ограничен и известен заранее.

Для senior-позиции важно обсудить trade-off между скоростью доступа, памятью и сложностью поддержки. В production чаще используют map с составным ключом, так как он даёт баланс между простотой и производительностью.

На практике

Выбор зависит от паттерна доступа:

  • Если нужен частый поиск по паре (customerId, accountNumber) - map с составным ключом.
  • Если нужны частые итерации по всем записям или сортировка - параллельные срезы.
  • Если нужны операции по всем счетам одного customerId - вложенные map.

Для статуса ареста используем int с константами (0 - нет ареста, 1 - арест, 2 - частичный арест). Это быстрее и экономит память по сравнению со string.

Важно продумать потокобезопасность: если коллекция используется конкурентно, нужен sync.RWMutex или шардирование map.

Пример кода

GO
package main
import (
"fmt"
"strconv"
"sync"
)
type ArrestStatus int
const (
StatusNoArrest ArrestStatus = iota
StatusArrested
StatusPartiallyArrested
)
// Вариант 1: map с составным ключом
type ArrestRegistry struct {
mu sync.RWMutex
data map[string]ArrestStatus
}
func NewArrestRegistry() *ArrestRegistry {
return &ArrestRegistry{
data: make(map[string]ArrestStatus),
}
}
func key(customerID int, accountNumber string) string {
return strconv.Itoa(customerID) + ":" + accountNumber
}
func (r *ArrestRegistry) Set(customerID int, accountNumber string, status ArrestStatus) {
r.mu.Lock()
defer r.mu.Unlock()
r.data[key(customerID, accountNumber)] = status
}
func (r *ArrestRegistry) Get(customerID int, accountNumber string) (ArrestStatus, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
status, ok := r.data[key(customerID, accountNumber)]
return status, ok
}
// Вариант 2: параллельные срезы (без потокобезопасности для простоты)
type SliceRegistry struct {
customerIDs []int
accountNums []string
statuses []ArrestStatus
}
func (s *SliceRegistry) Append(customerID int, accountNum string, status ArrestStatus) {
s.customerIDs = append(s.customerIDs, customerID)
s.accountNums = append(s.accountNums, accountNum)
s.statuses = append(s.statuses, status)
}
func (s *SliceRegistry) Find(customerID int, accountNum string) (ArrestStatus, bool) {
for i := range s.customerIDs {
if s.customerIDs[i] == customerID && s.accountNums[i] == accountNum {
return s.statuses[i], true
}
}
return StatusNoArrest, false
}
func main() {
reg := NewArrestRegistry()
reg.Set(123, "ACC-001", StatusArrested)
if status, ok := reg.Get(123, "ACC-001"); ok {
fmt.Printf("Статус: %d\n", status)
}
}

Как отвечать на собеседовании

Начни с уточнения требований: какой паттерн доступа, конкурентность, объём данных. Затем предложи map с составным ключом как базовый вариант, объясни почему. Упомяни альтернативы и их trade-off. Покажи понимание проблем: коллизии ключей, память, скорость. Если спросят про потокобезопасность - предложи RWMutex или шардирование. Для senior важно продемонстрировать системное мышление: не просто код, а обоснование выбора под конкретные условия.

Что проверяет интервьюер

  • Понимание структур данных и их сложности.
  • Умение выбирать между trade-off (map vs slice vs nested map).
  • Знание особенностей Go: отсутствие generics (в старых версиях), работа с примитивами, конкурентность.
  • Способность проектировать без ООП-парадигмы.
  • Внимание к деталям: разделители в ключах, обработка ошибок, потокобезопасность.

Типичные ошибки

  • Использование fmt.Sprintf в горячем пути - медленно, лучше strconv.
  • Забывают про потокобезопасность при конкурентном доступе.
  • Выбирают вложенные map без необходимости - избыточная память.
  • Не учитывают коллизии при конкатенации: если customerId и accountNumber могут содержать разделитель.
  • Хранят статус как string вместо int - лишние аллокации.
  • Не продумывают удаление записей - в map это просто, в срезах сложно.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью