Подходит ли слайс в Go для хранения и частого увеличения массива событий
Что проверяют
Знаете ли вы устройство слайса и стоимость append, и умеете ли отличать «подходит» от «подходит при таких-то условиях». Хороший ответ звучит как разбор сценария: как именно растёт коллекция, как читается, кто в неё пишет.
Как отвечать
Да, для накопления событий слайс это нормальный выбор. Слайс это структура из трёх полей: указатель на массив, длина и ёмкость. Когда длина упирается в ёмкость, append выделяет новый массив и копирует элементы, поэтому добавление стоит амортизированно O(1), а не O(n).
Дальше важные оговорки:
- если порядок величины известен, выделяйте заранее:
make([]Event, 0, n), это убирает лишние копирования; - коэффициент роста это деталь реализации рантайма, не называйте конкретное число, для больших слайсов он давно не двойка;
- после перевыделения старый массив не связан с новым, и указатели на прежние элементы ведут в никуда;
- срез от большого слайса удерживает весь исходный массив в памяти, освободить помогает копия в новый слайс.
Если события не только копятся, но и разбираются с начала, слайс хуже: удаление из головы это сдвиг всего хвоста. Тогда кольцевой буфер, а при обмене между горутинами буферизованный канал.
Где спотыкаются
Говорят, что слайс всегда растёт вдвое, и строят на этом расчёт памяти. Стратегия менялась между версиями и зависит от размера, полагаться на неё нельзя.
Забывают, что append возвращает новый слайс. Присвоили результат другой переменной, продолжили писать в исходный, и два слайса делят один массив, пока один из них не перевыделится. Такие места читаются как случайная порча данных.
Считают слайс безопасным для конкурентного доступа. Два append из разных горутин это гонка, она находится запуском тестов с детектором гонок. Сюда же удаление элемента сдвигом: в хвосте массива остаётся ссылка на последний элемент, и для указателей это удержанная память.