Содержание урока · 9 разделов
Каждый пункт этого урока — история, которая случалась в реальных проектах, и потому грабли стоит знать до встречи с ними. Семейство стилей — это файлы, которые пухнут и открываются в Excel десятками секунд: причина чаще всего одна, стили создаются в цикле. Семейство строк — паники на недозаполненных строках, пустые объединённые ячейки, съехавшие индексы. Семейство ссылок — формулы, которые ссылаются в никуда после переименования листа, и имена листов с пробелами без апострофов. Отдельная группа — дескрипторы: книга, открытая без отложенного Close, держит ресурсы до конца процесса, а проигнорированная ошибка SaveAs превращает рабочий отчёт в пустышку. Заметьте: ни одна из этих ошибок не проявляется в момент сборки — файл собирается без ошибок и выглядит нормально, а ломается он у пользователя или через сутки. Поэтому ниже для каждой ловушки есть три вещи: симптом, причина и лечение.
Ошибки excelize со стилями: NewStyle и таблица стилей#
Стиль в xlsx — это запись в общей таблице стилей книги, и NewStyle лишь добавляет туда запись и возвращает её номер. Дедупликации нет: два одинаковых вызова дают две записи, а вызов в цикле по ячейкам — таблицу на тысячи строк и файл, который Excel открывает мучительно. Ошибки excelize со стилями лечатся одним правилом: один NewStyle на вид оформления, кэш идентификаторов на время работы генератора. Вторая ловушка — применение: SetCellStyle работает и с ячейкой, и с диапазоном, поэтому заливка шапки A1:F1 — это один вызов, а не шесть. Третья — условное форматирование: у него свой конструктор NewConditionalStyle из урока про условное форматирование, и попытка передать туда идентификатор обычного стиля приводит к молчаливому нулю оформления. Четвёртая — форматы чисел: они задаются полем NumFmt или пользовательской строкой в структуре Style, а не строкой вроде «0.00» отдельным вызовом, и путаница здесь даёт ячейки, которые выглядят правильно, но не суммируются.
Кэш стилей: плохо и хорошо
GO// Плохо: стиль создаётся в цикле на каждую строку
for r := 2; r <= last; r++ {
s, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
})
f.SetCellStyle("Отчёт", "A"+strconv.Itoa(r), "F"+strconv.Itoa(r), s)
}
// Хорошо: один стиль на вид оформления, применение диапазоном
style, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
})
if err != nil {
panic(err)
}
f.SetCellStyle("Отчёт", "A2", "F"+strconv.Itoa(last), style)В плохом варианте таблица стилей растёт на каждую строку отчёта, хотя оформление одно и то же. В хорошем NewStyle вызывается один раз, а SetCellStyle красит весь диапазон одним вызовом: файл остаётся компактным, и Excel не тратит время на разбор тысяч одинаковых записей.
Повторный вызов SetCellStyle стирает предыдущий стиль ячейки: заливка, применённая первым вызовом, исчезнет после второго с жирным шрифтом. Несколько эффектов на одну ячейку собираются одной структурой Style с заполненными полями Font, Fill, Border и Alignment — один NewStyle, один вызов SetCellStyle. Это касается и правки: чтобы «добавить рамку», заново собирают полную структуру со всем оформлением, а не вызывают SetCellStyle второй раз.
Строки, колонки и объединённые ячейки#
Индексация в excelize — с единицы, и первая строка листа — это A1, а не A0: сдвиг на единицу всплывает в координатах, номерах строк в журналах ошибок и границах циклов. Хвостовые пустые ячейки вы уже встречали в уроке про импорт: срез строки из GetRows короче ширины листа, и прямое обращение по индексу паникует на первой же строке с дырой. Объединённые ячейки — отдельная история: значение живёт только в верхней левой ячейке диапазона, остальные пусты, и чтение правой части MergeCell даёт пустую строку даже там, где на экране Excel видно содержимое. Для работы с объединениями есть GetMergeCells — список всех диапазонов листа со стартовой и конечной осями и методом чтения значения. Отдельные грабли — вставка строк: InsertRows двигает содержимое, но ссылки в формулах за сдвинутыми строками, как правило, не пересчитывает, поэтому после вставки стоит открывать файл и проверять итоги. И последнее: колонки и строки в API именуются по-разному — буквы для колонок и числа для строк, и перепутанный порядок аргументов даёт запись не туда без всякой ошибки.
Файлы и дескрипторы: Close, SaveAs и чужие книги#
Книга excelize держит распакованные части архива в памяти и временные ресурсы, поэтому Close ставится через defer сразу после создания или открытия — это такой же рефлекс, как defer rows.Close у курсора базы. После Close файл не читается: вызов GetRows вернёт ошибку про закрытый архив, и это не паника, а тихая ошибка, которую легко пропустить, если её игнорировать. SaveAs проверяется в месте вызова: несуществующая директория, занятый файл или недопустимое имя листа вернут ошибку, а без проверки отчёт молча не сохранится, и обработчик ответит клиенту успехом. Открытие чужого файла — OpenFile — читает весь архив в память, и на гигантских файлах это лимит окружения, о котором мы говорили в уроке про производительность; защищённые книги открываются опцией с паролем. Формулы со ссылками на лист, чьё имя содержит пробел, требуют апострофов: формула со ссылкой на Итоги 2024 без кавычек-апострофов не сработает, а после RenameSheet стоит проверить ссылки на переименованный лист — часть этой работы библиотека берёт на себя, но проверка файла после переименования дешевле ночного инцидента.
Дисциплина работы с файлом
GOf, err := excelize.OpenFile(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
if err := f.SetCellFormula("Отчёт", "F10", "'Итоги 2024'!B2*1.2"); err != nil {
return err
}
if err := f.SaveAs(outPath); err != nil {
return fmt.Errorf("save %s: %w", outPath, err)
}Открытие, отложенное закрытие и проверенные ошибки — минимальная дисциплина работы с книгой. Ссылка на лист с пробелом в имени обёрнута в апострофы: без них формула не разберётся. Ошибки оборачиваются контекстом через fmt.Errorf, чтобы в логе было видно, на каком файле и на каком шаге всё остановилось.
Аптечка граблей#
| Симптом | Причина | Лечение |
|---|---|---|
| Файл открывается в Excel десятками секунд | тысячи записей в таблице стилей | кэш стилей: один NewStyle на вид оформления |
| Паника на обращении к ячейке строки | строка короче шапки: пустой хвост не в срезе | проверка len(row) или хелпер с пустотой |
| В объединённой ячейке пусто | значение лежит в верхней левой ячейке | GetMergeCells и чтение стартовой оси |
| Заливка пропала после добавления шрифта | SetCellStyle заменяет стиль целиком | одна структура Style со всеми полями |
| Отчёт не появился, ошибок нет | проигнорирована ошибка SaveAs | проверять err у SaveAs и Close |
| Формула ссылается в никуда | лист переименован или имя с пробелом | апострофы и проверка ссылок после переименования |
Культура проверки ошибок: пустой файл не выглядит пустым#
Почти каждый вызов excelize возвращает ошибку, и главная граблина — не в библиотеке, а в игнорировании: SetCellValue с опечаткой в имени листа вернёт ошибку, которую проглотило подчёркивание, и файл соберётся с дырой там, где по плану была колонка. Рабочая дисциплина простая: ошибки обрабатываются в месте вызова либо через маленькую обёртку, которая логирует координату и шаг генерации; подчёркивание допустимо только там, где отказ ячейки не влияет на результат, и это решение осознанное. Ошибки стоит разделять на два сорта: ошибки данных — битое значение, нестандартный формат — и ошибки библиотеки — неверное имя листа, нечитаемый файл; первые логируются и копятся, вторые останавливают генерацию. Отдельная линия обороны — тесты: чтение файла обратно ловит пропавшую колонку и пустую шапку на этапе CI, а не на телефоне заказчика. Грабли excelize в проверке ошибок не выглядят эффектно, но именно они отличают генератор, который доверяют ночным расписаниям, от генератора, за которым следят вручную.
GetCellValue возвращает пустую строку и для ячейки без значения, и для ячейки, которой не существует, — по строке ответа их не различить. Когда разница важна, проверяйте тип ячейки через GetCellType и структуру листа через GetRows: так отделяются дыры в данных от ошибок в координатах. Это же правило экономит время на отладке формул: пустая строка на месте итога чаще означает отсутствующую ячейку, чем ноль.
Чек-лист граблей#
- Один NewStyle на вид оформления: кэш идентификаторов вместо вызова в цикле.
- SetCellStyle заменяет стиль целиком: несколько эффектов — одна структура Style.
- len(row) перед индексами: хвостовые пустые ячейки в срезе не живут.
- Значение merged-ячейки — в верхней левой: чтение через GetMergeCells.
- defer Close сразу после открытия, ошибки SaveAs и Close проверяются в месте вызова.
- Имя листа с пробелом в формуле — в апострофах; после RenameSheet проверяем ссылки.
Половина ловушек этого урока — про строки и индексы, и они воспроизводятся в песочнице без файлов: возьмите срез срезов строк с дырами и короткими строками, напишите хелпер доступа с проверкой длины через strings и strconv, соберите журнал ошибок и убедитесь, что паники нет. Сам файл, стили и SaveAs гоняются локально — песочница файлы не пишет, и притворяться иначе не будем.
Кейс о том, как грабли складываются в штабель. Генератор прайс-листа вырос из скрипта: каждую строку красили стилем, созданным на месте, ошибки SetCellValue собирались в подчёркивание, а Close ставился только в одной из двух веток выхода. Месяцами всё держалось, пока файл не перевалил за тридцать мегабайт: таблица стилей выросла до десятков тысяч записей, Excel у менеджера открывал прайс почти минуту, а ночные прогоны начали ловить лимит открытых дескрипторов — вторая ветка выхода не закрывала книгу. Починка заняла день: кэш из трёх стилей, обёртка с логом координаты, defer на всех путях. Мораль кейса не в списке фиксов, а в порядке: симптом нашёлся у пользователя, причина — в трёх независимых местах, и лечились они одним навыком — проверять то, что возвращает библиотека. Спорная часть осталась в команде: кто-то хочет строгий режим, где первая же ошибка записи останавливает генерацию, кто-то — толерантный, где дыры собираются в отчёт; выбрали строгость для загрузок из базы и толерантность для импорта чужих файлов.
Проверка: почему файл тормозит
Сгенерированный отчёт стал открываться в Excel десятками секунд. Что проверить первым?
Проверка: merged cells
Что вернёт GetCellValue для правой ячейки объединённого диапазона?
Что станет с заливкой
Прочитайте код и выберите, что он выведет:
f := excelize.NewFile()
s1, _ := f.NewStyle(&excelize.Style{
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FFFF00"}},
})
s2, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
})
f.SetCellStyle("Sheet1", "B2", "B2", s1)
f.SetCellStyle("Sheet1", "B2", "B2", s2)Соберите дисциплину работы с файлом
Допишите открытие книги, закрытие, создание стиля и его применение к диапазону.
Соберите порядок работы с книгой
Расставьте шаги работы с книгой: открыть файл, отложить закрытие, создать стиль, применить к диапазону, записать значение, сохранить.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: грабли
Карта граблей пополнилась, и теперь она работает на профилактику, а не на разбор инцидентов. Устройство стилей и их кэша разобрано в уроке про стили ячеек, условное форматирование с собственным конструктором стиля — в уроке про условное форматирование, чтение чужих таблиц с хвостовыми пустыми ячейками — в уроке про импорт в структуры. Ссылки на листы и формулы подробно живут в уроке про формулы, а ловушка «молча не сохранилось» закрывается тестами из урока про тесты генерации. В следующем уроке — свойства книги: метаданные и настройки печати. Домашнее задание — пройдитесь по своему генератору с чек-листом урока и посчитайте, сколько пунктов он нарушает сегодня.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Один NewStyle на вид оформления: вызов в цикле раздувает таблицу стилей и тормозит Excel.
- SetCellStyle заменяет стиль целиком: несколько эффектов собираются одной структурой Style.
- len(row) перед индексами: хвостовые пустые ячейки в срезе строки не живут.
- Значение merged-ячейки лежит в верхней левой ячейке, диапазоны отдаёт GetMergeCells.
- defer Close сразу после открытия, ошибки SaveAs и Close проверяются в месте вызова.
- Ошибки делятся на данные и библиотеку: первые копятся, вторые останавливают генерацию.
Домашнее задание
Пройдите по своему генератору с чек-листом урока: посчитайте вызовы NewStyle, найдите подчёркивания на ошибках записи и проверьте, что Close стоит на всех путях выхода. Исправьте найденное, добавьте обёртку с логом координаты ячейки и убедитесь тестом чтения обратно, что содержимое не изменилось. Отдельно воспроизведите merged-ловушку: создайте объединение, прочитайте правую ячейку и напишите, что вернулось.
Личные заметкиПусто
Вопросы и ответы по уроку
Файл пухнет, хотя данных немного — где искать?
Считайте записи в таблице стилей: чаще всего причина в NewStyle внутри цикла. Уберите вызовы из цикла, кэшируйте идентификаторы стилей и применяйте их диапазоном через SetCellStyle — размер упадёт на порядки.
Почему после InsertRows формулы считают по старым строкам?
Вставка строк двигает содержимое, но пересчёт ссылок в формулах, как правило, остаётся на стороне Excel или вашего кода. После вставки проверяйте итоговые ячейки в открытом файле и при необходимости перестраивайте формулы в Go.
Как не пропускать ошибки записи в каждой строке кода?
Заведите маленькую обёртку, которая принимает ошибку, координату и шаг генерации: логирует проблему и решает — копить или останавливать. Это концентрирует политику ошибок в одном месте и убирает бесконечные if err != nil в бизнес-коде.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Фоновые задачи в Go: планировщик на robfig/cron
Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.
- Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
- Читать и писать cron-выражения: пять полей, спецсимволы, @daily
- Управлять жизненным циклом: Start, Stop, Entry, Remove
- Держать часовые пояса и секундные расписания под контролем