На файле в сто строк любые решения дают одинаковый результат: и обычная запись, и StreamWriter, и кэш стилей, и стили в цикле. Стоимость генерации растёт с числом ячеек, и проявляется она в нескольких местах: библиотека собирает XML-части книги в памяти, таблица стилей растёт вместе с числом созданных стилей, а сборщик мусора крутится на строковом мусоре — координатах, промежуточных срезах, копиях значений. Ни одно из этих мест не видно на код-ревью: файл собирается, тесты зелёные, и только под нагрузкой или на боевых объёмах выгрузка вдруг занимает гигабайты памяти. Поэтому порядок урока такой: сначала инструменты замера, потом карта расходов — где что теряется, затем лимиты, о которых стоит знать до релиза, и в конце план оптимизации, где каждое изменение подтверждается цифрами, а не ощущением.
Excelize производительность: чем измерять#
Первый инструмент — бенчмарк testing.B: тело с приёмом b повторяется фреймворком b.N раз, и Go печатает среднее время на операцию вместе с аллокациями. Бенчмарк отвечает на вопрос «сколько», но не на вопрос «где»: для второго существует pprof — профиль процессора и памяти, который показывает функции-рекордсмены. Грубая прикидка в один глаз — time.Now и time.Since вокруг подозрительного участка, а для памяти — runtime.ReadMemStats с полем HeapAlloc до и после генерации. Два правила замера экономят дни. Первое: мерить на данных, по форме похожих на боевые, — цифры, снятые на сотне строк, ничего не говорят о миллионе. Второе: мерить на одной и той же машине без параллельной нагрузки — числа, снятые рядом с пересборкой проекта, не сравниваются между собой. Excelize производительность — это не одна цифра, а соотношение работы библиотеки, вашего кода и окружения, и разницу замечают только при равных условиях.
Бенчмарк записи строк
GOfunc BenchmarkWriteRows(b *testing.B) {
for i := 0; i < b.N; i++ {
f := excelize.NewFile()
for r := 2; r <= 5001; r++ {
cell, _ := excelize.CoordinatesToCellName(1, r)
f.SetCellValue("Sheet1", cell, r)
}
_ = f.Close()
}
}Тело повторяется b.N раз, и go test -bench печатает среднее время на один проход вместе с аллокациями на операцию. CoordinatesToCellName собирает координату из номера колонки и строки — альтернатива fmt.Sprintf с меньшим строковым мусором; прогоните оба варианта и сравните ns/op и B/op на своей машине, а не по чужим таблицам.
Бенчмарк, снятый на ноутбуке, не переносится на сервер, а числа из чужой статьи не переносятся на вашу версию библиотеки: между релизами excelize менялись и внутренние пути записи, и накладные расходы. Сравнивайте не абсолютные величины, а отношения до и после одного изменения на одной машине: формулировка «стало в N раз» без описания стенда и объёма данных ничего не подтверждает. Шум сборщика мусора тоже смазывает картину — если разница между вариантами близка к разбросу прогонов, добавьте повторов и увеличьте benchtime.
Память excelize: где теряются гигабайты#
Первый расход — GetRows и его родственники: вызов собирает весь лист в срез срезов строк, и на листе в миллион строк такой срез занимает гигабайт плюс накладные расходы на каждую строку. Второй — стили: каждый вызов NewStyle добавляет запись в таблицу стилей книги, и вызов в цикле по ячейкам раздувает память при сборке, сам файл и время его открытия в Excel. Третий — итоговый документ в буфере: WriteToBuffer собирает файл целиком в памяти, и на сотнях мегабайт данных это удваивает расход, поэтому для больших выгрузок StreamWriter из урока про потоковую запись — рабочий инструмент, а не украшение. Четвёртый расход тише: строковый мусор — координаты ячеек, промежуточные срезы, копии значений; по отдельности мелочь, на миллионе строк — заметная доля, которую видно по аллокациям бенчмарка. Последний пункт простой: не держите открытыми десятки книг одновременно — каждая живёт со своими XML-частями до Close, и память excelize складывается по всем книгам, а не только по самым тяжёлым.
StreamWriter: плоская память на большом объёме
GOsw, err := f.NewStreamWriter("Sheet1")
if err != nil {
panic(err)
}
for r := 2; r <= 1000000; r++ {
cell, _ := excelize.CoordinatesToCellName(1, r)
sw.SetRow(cell, []interface{}{r})
}
if err := sw.Flush(); err != nil {
panic(err)
}StreamWriter пишет строки по ходу цикла и держит в памяти только служебные структуры, а не весь документ: пик памяти почти не растёт с числом строк, в отличие от обычной записи. Финальный Flush выгружает накопленное в файл — забытый Flush оставит лист пустым без ошибки в месте записи.
Снижение пиковой памяти оплачивается ограничениями: поток работает с новым файлом, пишет строки по порядку, не даёт читать написанное обратно и ставить стили ячейкам после записи. Для отчёта на несколько тысяч строк обычная запись остаётся удобнее и читается в обе стороны. Мигрируйте на StreamWriter, когда замер показал рост памяти при обычной записи, — не заранее, а по цифрам.
Что выбирать под объём#
| Инструмент | Объём | Особенность |
|---|---|---|
| SetCellValue в цикле | до десятков тысяч строк | простой код, файл читается и правится в обе стороны |
| StreamWriter | сотни тысяч строк и больше | память плоская, ограничения из урока про потоковую запись |
| GetRows | чтение маленьких листов | весь лист собирается в памяти процесса |
| Rows-итератор | чтение больших листов | в памяти живёт одна текущая строка |
| NewStyle в цикле | антипаттерн | таблица стилей растёт на каждую ячейку |
| NewStyle один раз | норма | один идентификатор на вид оформления, применение через SetCellStyle |
Лимиты: лист, файл и окружение#
У формата xlsx лимит листа — 1 048 576 строк и 16 384 колонки, и excelize вернёт ошибку, когда запись уйдёт за границу. Реальный потолок наступает раньше: память окружения — контейнер с двумя гигабайтами не соберёт отчёт, на который при сборке уходит четыре, а механизм изоляции убьёт процесс тихо, без стектрейса в логе. После сборки свой лимит наступает у читателя: файл на сотни мегабайт открывается в Excel ощутимо дольше, и человек с машиной попроще получает фактически недоступный отчёт. Практический вывод: узнайте лимиты своего окружения до релиза — память пода, ограничения на размер файла в интерфейсе, договорённость с заказчиком о том, что он вообще открывает. Разбивка на части, выгрузка частями по месяцам или архив с несколькими книгами — эти решения дешевле принять на этапе дизайна, чем на этапе инцидента ночью.
Чек-лист производительности#
- Бенчмарк testing.B и профиль pprof — основные инструменты, time.Since и HeapAlloc — быстрая прикидка.
- Замер делается на данных, похожих на боевые, и на одной машине для сравнимости прогонов.
- GetRows собирает весь лист: для больших листов чтение идёт через Rows-итератор.
- NewStyle — один раз на вид оформления, SetCellStyle — сколько нужно, включая диапазоны.
- Для огромных выгрузок — StreamWriter: память плоская, ограничения честные и описанные.
- Лимит листа — 1 048 576 строк, реальный потолок — память окружения и Excel читателя.
Механика замера тренируется прямо в песочнице: там доступны fmt, strings, strconv, math, sort, errors и time. Заведите срез на сотню тысяч строк, прогоните цикл между time.Now и time.Since, добавьте данные и посмотрите, как растёт время, — этот навык переносится на любые профили. Генерация файлов в песочнице не работает: бенчмарки с excelize и снимки памяти гоняются локально, на данных, похожих на ваши боевые.
План оптимизации: от замера к изменению#
Оптимизация без замера — лотерея, поэтому план короткий и дисциплинированный. Сначала базовый замер: бенчмарк на правдоподобных данных, время и память зафиксированы в заметке или в CI. Затем профиль: pprof покажет, куда уходит время, — иногда это не excelize, а запрос к базе, сериализация или ваш код преобразования значений, и тогда править нужно не библиотеку. Дальше одно изменение за раз: кэш стилей, итератор вместо GetRows, StreamWriter вместо обычной записи, CoordinatesToCellName вместо Sprintf — и сразу повторный замер по той же методике. Если цифры не сдвинулись, изменение откатывается или остаётся по другим соображениям, например по читаемости. В конце — тесты содержимого: оптимизация, которая сломала файл, не считается оптимизацией, и здесь пригодится урок про тесты генерации, где результат проверяется чтением обратно и golden-снимками. Так excelize производительность улучшается шагами, каждый из которых объясняется цифрой коллегам и вам самим через месяц.
Кейс из жизни сервиса выгрузок. Ночной экспорт держал четыре гигабайта памяти и падал в поде с лимитом два: разработчик собирал сводку из листа-источника через GetRows на весь лист, а каждую строку шапки красил стилем, создаваемым в цикле. Первое изменение было минимальным — кэш стилей из урока про стили: один NewStyle на шапку, один на зебру, идентификаторы переиспользуются. Второе — чтение источника перевели на Rows-итератор из урока про потоковое чтение, и память перестала масштабироваться с размером источника. Третьим шагом саму выгрузку переписали на StreamWriter, и пик опустился до сотен мегабайт. Каждый шаг фиксировался замером на одном и том же наборе данных, потому что после первого же спора «кажется, быстрее» команда перешла на сравнение отношений до и после. Показательный итог: два шага из трёх касались не библиотеки, а того, как вокруг неё построен код — excelize платила за чужие решения.
Проверка: GetRows и память
Что произойдёт, если вызвать GetRows на листе в миллион строк?
Проверка: стили в цикле
Чем плох вызов NewStyle для каждой записываемой ячейки?
Сколько стилей в файле
Прочитайте код и выберите, что он выведет:
f := excelize.NewFile()
for i := 0; i < 3000; i++ {
style, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
})
f.SetCellStyle("Sheet1", "A1", "A1", style)
}
_ = f.Close()Соберите замер потоковой записи
Допишите создание потока, финализацию, снимок памяти и вычисление длительности.
Соберите каркас замера
Расставьте строки каркаса замера: засечь время, создать книгу, отложить закрытие, собрать координату, записать значение, напечатать длительность.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: производительность
Дисциплина замера — навык на всю работу с генерацией файлов, а не только на этот курс. Механика потоковой записи разобрана в уроке про StreamWriter, чтение больших листов — в уроке про потоковое чтение, а устройство кэша стилей и виды оформления — в уроке про стили ячеек. Скелет экспорта, вокруг которого мы мерили ресурсы, собран в уроке про экспорт из базы, а проверка результата замерами содержимого — в уроке про тесты генерации. Дальше по курсу — коллекция типичных ошибок, где многие грабли перекликаются с расходами этого урока. Домашнее задание — снять базовый замер своей выгрузки и сравнить два варианта записи на одном стенде.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Замер предшествует оптимизации: бенчмарк testing.B, профиль pprof, быстрая прикидка time.Since и HeapAlloc.
- GetRows собирает весь лист в память — большие листы читают итератором Rows.
- NewStyle не дедуплицируется: стиль создают один раз, SetCellStyle применяют сколько нужно.
- StreamWriter держит память плоской, но ограничен новым файлом и порядком строк.
- Лист xlsx — до 1 048 576 строк; реальный потолок задаёт память окружения и Excel читателя.
- Одно изменение — один замер: оптимизация подтверждается цифрами и не ломает тесты содержимого.
Домашнее задание
Снимите базовый замер своей выгрузки: бенчмарк на правдоподобных данных через go test -bench, память через HeapAlloc до и после генерации. Затем внесите одно изменение из урока — кэш стилей, итератор вместо GetRows или StreamWriter вместо обычной записи — и повторите замер по той же методике. Сравните ns/op и B/op, запишите выводы и убедитесь, что тесты содержимого файла не сломались.
Личные заметкиПусто
Вопросы и ответы по уроку
Экспорт тормозит, и непонятно почему — с чего начать?
С профиля: pprof по процессору покажет, находится ли узкое место в excelize или рядом — в запросе к базе, сериализации и вашем коде преобразования значений. Оптимизация без профиля — правка наугад, и чаще всего она промахивается.
GetRows на большом листе падает по памяти — что делать?
Перейдите на Rows-итератор из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку. Результат пишите в базу или файл по ходу разбора, не копя срез на весь лист.
StreamWriter обязателен для больших файлов?
Он рабочий инструмент для сотен тысяч строк, когда замер показывает рост памяти при обычной записи. На малых объёмах ограничения потока не оправданы: обычная запись удобнее, допускает чтение обратно и стили без ограничений.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Фоновые задачи в Go: планировщик на robfig/cron
Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.
- Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
- Читать и писать cron-выражения: пять полей, спецсимволы, @daily
- Управлять жизненным циклом: Start, Stop, Entry, Remove
- Держать часовые пояса и секундные расписания под контролем