Какие проблемы могут возникнуть при возвращении внутренней мапы напрямую в Go
Что проверяют
Понимаете ли вы, что map в Go ссылается на внутреннюю структуру рантайма и её возврат из метода открывает наружу состояние, которое вы собирались защищать. Следом обычно просят написать безопасный геттер и спрашивают, чем плоха копия.
Как отвечать
Начните с модели: значение типа map это указатель на хеш-таблицу рантайма. Копия переменной не копирует данные, вызывающий код получает ту же таблицу. Отсюда проблемы:
- нарушение инкапсуляции: снаружи можно добавить, удалить или поменять значения в обход методов структуры. Валидация в
Set()больше ничего не гарантирует; - гонки: внутри стоит
sync.RWMutex, геттер берётRLockи возвращает map. Блокировка отпущена наreturn, дальше вызывающий читает таблицу, пока другая горутина в неё пишет. Рантайм часто ловит это какfatal error: concurrent map read and map write, и это не паника:recoverне спасёт, процесс падает целиком. В тестах гонку покажетgo test -race; - нелокальные баги: запись в одном месте, итерация в другом, и ошибка проявляется в коде, который структуру вообще не трогал.
Решения по возрастанию строгости:
- копия:
maps.Cloneиз стандартной библиотеки, сделанная под блокировкой. Копия поверхностная: указатели, слайсы и вложенные map в значениях остаются общими; - точечные методы:
Get(key),Len(),Range(func(k, v) bool)вместо выдачи всей таблицы; - итератор:
iter.Seq2через range-over-func. Тогда решите, держит ли он блокировку весь обход: если держит, тело цикла у вызывающего, которое зовётSet(), получит дедлок.
Копия стоит O(n) на каждый вызов, поэтому на горячем пути лучше точечные методы.
Где спотыкаются
Говорят, что map «передаётся по ссылке». В Go всё передаётся по значению, просто значение map само является указателем. Это любят уточнять.
Считают, что RLock внутри геттера защищает данные и после возврата.
Делают maps.Clone и успокаиваются, хотя в map[string]*User сами пользователи остаются общими и меняются снаружи.