Обычные методы записи, которые мы применяли до этого урока, держат лист в памяти целиком: каждая ячейка живёт структурой, и на сотнях тысяч строк этих структур миллионы. Для отчёта на две тысячи строк это незаметно, для выгрузки склада или лога звонков — уже проблема: генерация съедает гигабайты и натыкается на лимиты окружения. StreamWriter решает задачу механикой письма, а не оптимизацией объектов: разметка листа пишется чанками по мере вызова SetRow, крупные чанки уходят во временные файлы на диске, и память держится около постоянной независимо от числа строк. Плата за это — дисциплина: поток пишет лист за один проход, строки строго по возрастанию номеров, вернуться и дописать верх не получится, а обычные методы записи на тот же лист во время потока смешивать нельзя. Разберём полный цикл: открыть поток, записать шапку и данные, закрыть Flush, сохранить файл — и все ограничения, которые идут в комплекте.
NewStreamWriter: excelize streamwriter для пустого листа#
Поток открывается на существующем пустом листе: f.NewStreamWriter с именем листа возвращает StreamWriter и ошибку. Дальше обычный режим и режим потока на одном листе не смешиваются: SetCellValue на потоковом листе при незавершённом потоке — ошибка или молчаливая порча, а чтение ячеек до Flush вернёт пустоту, потому что разметка ещё не дописана. Завершение обязательное: sw.Flush() дописывает хвост разметки листа, и только после него работает f.SaveAs — забыть Flush значит собрать файл без данных. Стили при этом создаются привычным NewStyle на самой книге: стиль — ресурс книги, а не листа, и его идентификатор свободно используется в потоке. Запрос excelize streamwriter на практике сводится к четырём вызовам: NewStreamWriter, серия SetRow, Flush, SaveAs — остальное нуждается в понимании деталей, о которых дальше.
Полный цикл: от шапки до SaveAs
GOf := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
sw, err := f.NewStreamWriter("Sheet1")
if err != nil {
panic(err)
}
if err := sw.SetColWidth(1, 4, 22); err != nil {
panic(err)
}
header, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
})
if err := sw.SetRow("A1", []interface{}{
excelize.Cell{StyleID: header, Value: "Товар"},
excelize.Cell{StyleID: header, Value: "Количество"},
excelize.Cell{StyleID: header, Value: "Цена"},
excelize.Cell{StyleID: header, Value: "Сумма"},
}, excelize.RowOpts{Height: 24}); err != nil {
panic(err)
}
for i := 2; i <= 100001; i++ {
cell, _ := excelize.CoordinatesToCellName(1, i)
if err := sw.SetRow(cell, []interface{}{
"Товар " + strconv.Itoa(i),
i,
99.9,
excelize.Cell{Formula: "=B" + strconv.Itoa(i) + "*C" + strconv.Itoa(i)},
}); err != nil {
panic(err)
}
}
if err := sw.Flush(); err != nil {
panic(err)
}
if err := f.SaveAs("big.xlsx"); err != nil {
panic(err)
}Ста тысяч строк с формулой в каждой — и ни одного SetCellValue. Ширины заданы до строк, шапка получила стиль и высоту через RowOpts, данные пишутся по возрастанию, Flush завершает поток, и только потом файл сохраняется. Формулы в ячейках Сумма посчитает Excel при открытии — excelize, как и в обычном режиме, их не вычисляет.
SetRow требует номера строки строго больше предыдущей: пропуски допустимы, а возврат наверх вернёт ошибку. Потоковый лист живёт в одном направлении — сверху вниз, и структура строки должна быть известна к моменту записи: дописать колонку в уже ушедшие строки нечем. Обычные функции записи и поток на одном листе не совмещаются; на соседних листах книги — пожалуйста. И Flush — не опция, а часть контракта: без него файл соберётся без данных листа.
Cell, RowOpts и стили в потоке#
Срез значений SetRow heterogeneous: элементом может быть число, строка, time.Time, nil или специальная структура excelize.Cell с полями StyleID, Value и Formula — она нужна, когда ячейке нужен стиль или формула. nil пропускает ячейку целиком: в разметке её не будет, и это способ оставлять дыры без пустых значений. Богатый текст задаётся срезом RichTextRun прямо в значении — как в обычном режиме. Второй уровень настройки — excelize.RowOpts в переменном аргументе SetRow: высота строки Height, стиль всей строки StyleID и признак Hidden. Зебра на больших выгрузках делается комбинацией: две заготовки Cell с разными StyleID выбираются по чётности номера строки, а высота шапки задаётся RowOpts один раз. Запрос на большие excel файлы обычно упирается именно в это место: цикл, который собирает срез значений из данных и ставит на них стили идентификаторами, а не объектами.
Строки из данных с зеброй
GOzebra, _ := f.NewStyle(&excelize.Style{
Fill: excelize.Fill{Type: "pattern", Pattern: 1,
Color: []string{"F2F2F2"}},
})
for i, item := range items {
rowIdx := i + 2
cell, _ := excelize.CoordinatesToCellName(1, rowIdx)
var sum interface{}
if item.Sum > 0 {
sum = excelize.Cell{Value: item.Sum}
}
opts := []interface{}{item.Name, item.Count, sum}
if rowIdx%2 == 0 {
opts = append(opts, excelize.Cell{StyleID: zebra, Value: "итог"})
} else {
opts = append(opts, nil)
}
if err := sw.SetRow(cell, opts); err != nil {
panic(err)
}
}Срез значений собирается из данных, а пустые места уходят nil: ячейка просто не попадёт в файл. Зебра проставляется идентификатором стиля по чётности строки — на сотнях тысяч строк это единственный разумный способ, стиль один, а ссылок на него миллионы.
Что StreamWriter умеет помимо строк#
Поток — не голая труба строк: у него есть набор методов для оформления. sw.SetColWidth задаёт ширины колонок до записи строк, sw.SetColStyle красит целые колонки — тоже до строк. Закрепление областей ставится sw.SetPanes с той же структурой Panes, что в обычном режиме, — шапка большой выгрузки закрепляется прямо в потоке. Объединение ячеек под лозунг шапки делает sw.MergeCell. Таблицу с фильтрами добавляет sw.AddTable с теми же Table, что и обычный AddTable, с оговорками: таблица в потоке одна, и вызов происходит после записи строк, но до Flush. Разрывы страниц вставляет sw.InsertPageBreak. Чего в потоке нет — произвольных правок ушедших строк, условного форматирования и диаграмм: их место в слоях до или после потоковой выгрузки данных, и это осознанный компромисс ради памяти.
Память против скорости: честные ожидания#
Механика памяти такая: разметка пишется чанками в буфер, а когда буфер переполняется, StreamWriter уходит во временные файлы на диске, поэтому при выгрузке в миллионы строк процесс не раздувается — он платит диском вместо оперативки. Это гарантия про память, но не обещание скорости: время сборки зависит от числа ячеек, стилей и диска, и мерить его стоит на своих данных, а не на чужих бенчмарках. Дисциплина потока тоже имеет цену: структуру листа надо знать до записи, потому что вернуться наверх нечем, и команды, привыкшие дописывать колонки постфактум, перестраивают порядок кода. Практичная схема для огромных выгрузок: данные собираются запросом по курсору, каждая строка уходит в SetRow сразу после чтения из базы, никаких промежуточных срезов всего отчёта — тогда память живёт в одном месте, и файл собирается по мере прихода данных.
Методы StreamWriter#
| Метод | Пример | Смысл |
|---|---|---|
| NewStreamWriter | f.NewStreamWriter("Sheet1") | переводит пустой лист в режим потока |
| SetRow | sw.SetRow("A2", values) | пишет строку целиком, по возрастанию номеров |
| SetColWidth | sw.SetColWidth(1, 4, 22) | ширины колонок до записи строк |
| SetPanes | sw.SetPanes(&excelize.Panes{...}) | закрепление шапки прямо в потоке |
| AddTable | sw.AddTable(&excelize.Table{...}) | таблица с фильтром; одна, до Flush |
| Flush | sw.Flush() | завершает поток; после него работает SaveAs |
Cell с полем Formula пишет в ячейку текст формулы — и на этом останавливается: excelize формулы не вычисляет ни в обычном режиме, ни в потоке. В сохранённом файле кэша значений не будет, Excel или LibreOffice посчитают их при открытии, а парсер прочтёт строку с равенством. Для выгрузок, где итог нужен машине, считайте сумму в Go и пишите значением — правило из урока про формулы работает и здесь.
Чек-лист потоковой записи#
- NewStreamWriter открывает поток на пустом листе; обычные методы записи на тот же лист не смешивать.
- SetRow пишет строку целиком: срез значений, Cell для стилей и формул, nil пропускает ячейку.
- Номера строк строго по возрастанию; пропуски допустимы, возврат наверх — ошибка.
- Ширины, стили колонок и закрепление ставятся до строк, таблица — после строк и до Flush.
- Flush обязателен: без него файл соберётся без данных листа.
- Поток экономит память через временные файлы на диске; скорость меряйте на своих данных.
StreamWriter требует книги и диска, поэтому в браузерной песочнице не запускается: там доступны fmt, strings, strconv, math, sort, errors и time без файлового ввода-вывода. Разминка на тему потока — сборка строк: цикл формирует срез значений из структур через strconv и fmt, как это делает SetRow перед записью, а сам файл соберите локально.
Кейс про выгрузку, которая роняла контейнер. Логистический сервис собирал ежедневную выгрузку движения товара: к полумиллиону строк добавились новые колонки, и генерация на обычных методах стала есть несколько гигабайт — под-лимит пода Kubernetes в два гигабайта падал на середине. Переход на StreamWriter занял два дня, из них полтора — перестройка кода: раньше данные сначала собирались в большой срез структур, потом писались; в потоке строки ушли в SetRow прямо из курсора базы, и промежуточный срез исчез вместе с памятью под него. Сюрпризом стало то, что нельзя дописать колонку в шапку после начала данных: порядок кода перестроили — сначала все заголовки, потом данные. Спорная сторона осталась: часть команды считает, что для файлов больше сотни тысяч строк стоило писать CSV и не возиться с xlsx; оппоненты отвечают, что получатели живут в Excel и просят фильтры и закреплённую шапку. В итоге живут оба пути: для людей — StreamWriter с оформлением, для машинных загрузок — лёгкий CSV.
Проверка: порядок строк
Что произойдёт при вызовах sw.SetRow("A5", ...) и затем sw.SetRow("A2", ...) в одном потоке?
Проверка: забыли Flush
Данные записаны циклом SetRow, после чего код сразу вызвал f.SaveAs без sw.Flush(). Что соберётся?
Что вернёт SetRow наверх?
Прочитайте код и выберите, что он выведет:
sw, _ := f.NewStreamWriter("Sheet1")
sw.SetRow("A5", []interface{}{"пятая строка"})
err := sw.SetRow("A2", []interface{}{"вторая строка"})
fmt.Println(err)Соберите потоковую выгрузку
Допишите открытие потока, запись строки, завершение потока и сохранение файла.
Соберите цикл потоковой записи
Расставьте строки в порядке выполнения: открыть поток, задать ширину, записать шапку, записать строку данных, завершить поток, сохранить файл.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: StreamWriter
Потоковая запись закрыта: шапка и стили в потоке, сотни тысяч строк без роста памяти, Flush и SaveAs на месте, ограничения названы честно. Запись — половина истории про объёмы: в уроке про потоковое чтение тот же подход встретится с обратной стороны — итератор Rows прочитает огромный файл построчно, не укладывая его в память. Готовый шаблон, где потоковый режим живёт рядом с базой данных, соберём в уроке про экспорт отчёта из базы, а числа из строк и координаты ячеек вспомнятся из урока про GetRows. Измерения памяти и лимиты окружения подробно разберёт урок про производительность, и все слои встретятся в итоговом проекте. Домашнее задание — переписать свою выгрузку на StreamWriter и сравнить память до и после.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- NewStreamWriter переводит пустой лист в режим потока: обычные методы записи на тот же лист не смешиваются.
- SetRow пишет строку целиком по возрастанию номеров; Cell даёт стиль и формулу, nil пропускает ячейку.
- Ширины и стили колонок ставятся до строк, закрепление и объединение доступны в потоке, таблица одна и до Flush.
- Flush обязателен: он завершает разметку листа, и только после него работает SaveAs.
- Поток держит память около постоянной через чанки и временные файлы на диске.
- Формулы в потоке не считаются: текст пишет excelize, числа — Excel при открытии.
Домашнее задание
Возьмите свою выгрузку из прошлых уроков или сгенерируйте синтетику на сто тысяч строк: соберите её двумя способами — обычными SetCellValue и через StreamWriter с шапкой, зеброй по чётности и формулой суммы в колонке. Замерьте память процесса в обоих вариантах, убедитесь, что без Flush файл пустой, а после Flush открывается в Excel, где посчитаны формулы, и шапка закреплена, если добавили SetPanes в поток.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли смешивать SetCellValue и StreamWriter на одном листе?
Нет: лист в режиме потока пишется только через поток, обычные методы записи на него не вызываются. На соседних листах книги обычный режим работает свободно. Чтение ячеек потокового листа до Flush тоже не вернёт данных.
Нужно ли писать строки строго подряд, без пропусков?
Подряд не обязательно: пропуски допустимы, главное — номера строк строго по возрастанию. Возврат к уже пройденной строке вернёт ошибку, потому что поток пишет разметку по мере вызовов и не вернётся назад.
StreamWriter быстрее обычной записи?
Гарантия у потока про память, а не про скорость: время сборки зависит от объёма, стилей и диска. На больших файлах разница заметна, но мерить стоит на своих данных; на маленьких отчётах обычные методы проще и ни в чём не проигрывают.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Фоновые задачи в Go: планировщик на robfig/cron
Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.
- Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
- Читать и писать cron-выражения: пять полей, спецсимволы, @daily
- Управлять жизненным циклом: Start, Stop, Entry, Remove
- Держать часовые пояса и секундные расписания под контролем