Проект ставит задачу, а не повторяет уроки: на вход приходит срез продаж, на выходе — файл, который не стыдно отдать заказчику. Внутри сойдутся все слои курса: типы и данные из уроков про запись и чтение, стили и форматы, итоги формулой, диаграмма, свойства и печать, тесты чтения обратно. Формат урока — обзорная сборка: мы пройдём по структуре пакета и ключевым решениям, а построчные детали остаются в своих уроках, и на каждом шаге будет ссылка. Три критерия готовности: файл собирается командой, тесты проверяют содержимое через чтение обратно, и каждый новый вид оформления появляется в коде одним местом, а не разбросом вызовов. Если после сборки захочется продолжить — в кейсе в конце список расширений: REST-эндпоинт, потоковая запись для больших объёмов и конфигурация колонок снаружи.
Excelize проект: требования и план работ#
Задание проекта: функция, которая принимает срез продаж и возвращает xlsx с шапкой на заливке, зеброй по чётным строкам, заданными ширинами колонок, закреплённой шапкой, автофильтром, итоговой строкой с формулой SUM, столбчатой диаграммой по регионам, свойствами документа и настройками печати. Генерация отчетов go такого масштаба — это не один файл на тысячу строк, а пакет с ясными границами: типы данных, сборщик, доводка, тесты. План работ выглядит так: описать типы и каркас сборщика, собрать стили и шапку, записать строки с зеброй, поставить итоги, добавить диаграмму и доводку, покрыть тестами. Урок — обзорная сборка: каркас и решения на месте, а подробности каждого шага живут в своих уроках курса, и по ходу будут ссылки. Такой формат честнее построчного туториала: вы видите, как части соединяются, и сами дописываете детали — это и есть ежедневная работа генератора отчётов.
Каркас пакета report
GOpackage report
type Row struct {
Region string
SKU string
Qty int
Price float64
}
type Builder struct {
f *excelize.File
rows []Row
styles map[string]int
}
func New(rows []Row) (*Builder, error) {
f := excelize.NewFile()
if _, err := f.NewSheet("Отчёт"); err != nil {
return nil, err
}
return &Builder{
f: f,
rows: rows,
styles: map[string]int{},
}, nil
}
func (b *Builder) Close() error {
return b.f.Close()
}Тип Row описывает строку данных, Builder держит книгу, срез строк и карту стилей, а New создаёт лист отчёта и возвращает готовый к работе сборщик. Close освобождает ресурсы по правилам из урока про типичные ошибки: книга живёт ровно столько, сколько нужен генератор.
Каркас сборки: стили, шапка и зебра#
Сборка идёт по слоям, и первый слой — оформление. initStyles создаёт стили и складывает их в карту: шапка с заливкой и белым жирным шрифтом, зебра для чётных строк, дальше той же картой добавляется формат для денежных колонок. Правило из урока про производительность работает в полный рост: один NewStyle на вид оформления, вызовов в цикле нет. Шапка записывается одним срезом значений и красится диапазоном, ширины колонок задаются SetColWidth до записи строк, закрепление шапки — SetPanes из урока про закрепление, автофильтр — последним слоем. Строки данных пишутся SetSheetRow: метод разворачивает срез значений в ячейки строки одним вызовом и избавляет от четырёх SetCellValue на строку. Зебра применяется по чётному индексу диапазоном строки. Отдельное честное замечание: проект принимает срез строк и собирает файл в памяти — этого хватает отчётам на десятки тысяч строк, а для больших объёмов чтение переводится на курсор базы из урока про экспорт, а запись — на StreamWriter из урока про потоковую запись.
Слои сборки: стили, строки, итоги
GOfunc (b *Builder) initStyles() error {
header, err := b.f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
Alignment: &excelize.Alignment{Horizontal: "center"},
})
if err != nil {
return err
}
b.styles["header"] = header
zebra, err := b.f.NewStyle(&excelize.Style{
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"F2F2F2"}},
})
if err != nil {
return err
}
b.styles["zebra"] = zebra
return nil
}
func (b *Builder) addRows() error {
for i, r := range b.rows {
row := i + 2
cells := []interface{}{r.Region, r.SKU, r.Qty, r.Price}
if err := b.f.SetSheetRow("Отчёт",
fmt.Sprintf("A%d", row), &cells); err != nil {
return err
}
if i%2 == 1 {
if err := b.f.SetCellStyle("Отчёт",
fmt.Sprintf("A%d", row), fmt.Sprintf("D%d", row),
b.styles["zebra"]); err != nil {
return err
}
}
}
return nil
}
func (b *Builder) addTotals() error {
last := len(b.rows) + 1
total := last + 1
if err := b.f.SetCellValue("Отчёт",
fmt.Sprintf("A%d", total), "Итого"); err != nil {
return err
}
if err := b.f.SetCellStyle("Отчёт",
fmt.Sprintf("A%d", total), fmt.Sprintf("D%d", total),
b.styles["header"]); err != nil {
return err
}
return b.f.SetCellFormula("Отчёт",
fmt.Sprintf("D%d", total),
fmt.Sprintf("SUM(D2:D%d)", last))
}
func (b *Builder) Build() error {
if err := b.initStyles(); err != nil {
return err
}
if err := b.addHeader(); err != nil {
return err
}
if err := b.addRows(); err != nil {
return err
}
return b.addTotals()
}Карта стилей наполняется один раз и читается по имени вида оформления: NewStyle в цикле по данным не появляется. SetSheetRow пишет строку целиком, зебра красится диапазоном A:D по чётным индексам, а итоговая строка получает подпись, стиль шапки и формулу SUM по диапазону данных — значение посчитает Excel при открытии, тест сверит текст формулы. Build собирает слои в порядке урока про экспорт, и каждая ошибка возвращается наверх с местом возникновения.
Генерация файлов пишет на диск, поэтому сборка проекта выполняется локально: go run, файл рядом с бинарём, предпросмотр в Excel или LibreOffice. Песочница файлы не пишет — и честно об этом предупреждает, — но части проекта там тренируются в полный рост: посчитайте суммы по регионам из среза структур через strconv и math, отсортируйте sort.Slice, снимите JSON-снимок для golden-сравнения через fmt. Sort, math, errors, strings и time в песочнице есть, а сборка xlsx с диаграммой и печатью — локальная работа: это та самая честная граница между логикой данных и логикой файла.
Доводка: диаграмма, свойства и печать#
Второй этап — Polish: слой удобств, который превращает таблицу в отчёт. Диаграмма добавляется AddChart: столбчатый тип по регионам, категории из колонки A, значения из колонки D, подпись сверху — синтаксис ссылок и серий разобран в уроке про диаграммы. Свойства документа задаются DocProps с названием отчёта и периодом, область печати — по фактической границе данных, первая строка повторяется на каждой странице, колонтитул показывает название слева и номера страниц справа — всё это из урока про метаданные и печать. Ширины колонок и закрепление уже стоят с первого этапа, автофильтр включается по шапке. Порядок доводки тот же, что и в уроках: сначала данные и оформление, затем удобства, затем сохранение. Отдельно отметим компоновку: диаграмма ставится правее данных, чтобы печать области не втаскивала её в каждую страницу, а заголовок отчёта живёт в свойствах, а не в объединённой ячейке — за это правило кто-то поспорит, и это хороший спор для ревью.
Polish: диаграмма и печатный вид
GOfunc (b *Builder) Polish(title string) error {
last := len(b.rows) + 1
_, err := b.f.AddChart("Отчёт", "F2", &excelize.Chart{
Type: excelize.Col,
Series: []excelize.ChartSeries{
{
Name: "Продажи",
Categories: fmt.Sprintf("Отчёт!$A$2:$A$%d", last),
Values: fmt.Sprintf("Отчёт!$D$2:$D$%d", last),
},
},
Title: []excelize.RichTextRun{{Text: "Продажи по регионам"}},
})
if err != nil {
return err
}
if err := b.f.SetDocProps(&excelize.DocProps{
Title: title,
Creator: "report-bot",
Keywords: "продажи, сводка",
}); err != nil {
return err
}
if err := b.f.SetPrintArea("Отчёт",
fmt.Sprintf("A1:D%d", last+1)); err != nil {
return err
}
if err := b.f.SetPrintRows("Отчёт", "1:1"); err != nil {
return err
}
return b.f.SetHeaderFooter("Отчёт", &excelize.HeaderFooter{
OddFooter: "&L" + title + "&R&P из &N",
})
}Диаграмма ссылается на диапазоны данных через долларовые координаты, свойства получают название и источник, а печать ограничена областью данных с повтором шапки и колонтитулом. Каждый вызов проверяет ошибку: имя листа и диапазоны ловятся на месте сборки, а не в руках пользователя.
Тесты проекта: что и как проверяем#
Проект без тестов — не проект, а скрипт. Первый уровень — чтение обратно из урока про тесты генерации: WriteToBuffer и OpenReader, шапка, первая и последняя строка данных, текст итоговой формулы через GetCellFormula. Свойства проверяются GetDocProps: название и автор ловят забытую доводку. Стили — через GetCellStyle по идентификаторам из карты: тест красит шапку и зебру, и потерянное оформление падает на CI, а не в письме заказчика. Второй уровень — golden-снимок значений в testdata: срез строк сериализуется в JSON, и любое незапланированное изменение содержимого видно в diff. Третий — таблица кейсов на данных: пустой срез, одна строка, отрицательные количества; грани данных ломают генераторы чаще, чем средние значения. На тесты проекта стоит потратить столько же времени, сколько на сборку: пакет, который можно спокойно менять, переиспользуют, а пакет без тестов переписывают с нуля при первом же новом требовании.
Тест генератора
GOfunc TestSalesReport(t *testing.T) {
rows := []report.Row{
{Region: "Север", SKU: "A-1", Qty: 3, Price: 199.9},
{Region: "Юг", SKU: "B-2", Qty: 1, Price: 59.0},
}
b, err := report.New(rows)
if err != nil {
t.Fatal(err)
}
defer b.Close()
if err := b.Build(); err != nil {
t.Fatal(err)
}
buf, err := b.WriteToBuffer()
if err != nil {
t.Fatal(err)
}
rf, err := excelize.OpenReader(buf)
if err != nil {
t.Fatal(err)
}
defer rf.Close()
got, err := rf.GetCellValue("Отчёт", "A1")
if err != nil {
t.Fatal(err)
}
if got != "Регион" {
t.Errorf("A1: got %q, want Регион", got)
}
}Тест собирает отчёт из двух строк, выгружает книгу в буфер и открывает обратно без диска. Проверка шапки — минимальный шаг: рядом по той же схеме ставятся крайние строки, текст формулы GetCellFormula и свойства GetDocProps.
Этапы проекта и их проверки#
| Этап | Что делает | Чем проверяется |
|---|---|---|
| Типы и каркас | Row, Builder, New, Close | компиляция и Close без утечек |
| Стили | карта трёх видов оформления | GetCellStyle на шапке и зебре |
| Строки | SetSheetRow и зебра диапазоном | чтение первой и последней строки |
| Итоги | подпись и формула SUM | GetCellFormula и стиль итоговой строки |
| Доводка | диаграмма, DocProps, печать | GetDocProps и ручной предпросмотр |
| Тесты | чтение обратно и golden | go test в CI на каждый коммит |
Критерии сдачи проекта#
- Пакет собирается командой, файл открывается в Excel и LibreOffice без предупреждений.
- Стили живут в карте: ни одного NewStyle в цикле по данным.
- Итоговая строка с формулой SUM по диапазону данных, тест сверяет текст формулы.
- Диаграмма ссылается на диапазоны долларовыми координатами и подписана.
- DocProps заполнен, область печати и повтор шапки заданы, колонтитулы на месте.
- Тесты чтения обратно и golden-снимок проходят в CI до слияния ветки.
Кейс про доводку до продукта. Команда собрала генератор по этой схеме за вечер и положила в репозиторий как внутреннюю библиотеку. Первое расширение пришло через неделю: выгрузке понадобился REST-эндпоинт, и пакет прирос хендлером, который принимает фильтры, дергает базу и отдаёт файл — Builder к этому не готовился, но тесты чтения обратно поймали одну правку ширины колонок. Второе расширение — конфигурация колонок снаружи: список колонок с заголовками и форматами переехал в YAML, и addHeader стал строиться по описанию. Третье — большие выгрузки: отчёт за год не влезал в память обычной записи, и Builder переполз на StreamWriter из урока про потоковую запись с сохранением тестов. Через полгода библиотеку показали на собеседовании в портфолио, и главный вопрос был не про код, а про решения: почему стили в карте, почему формула, а не число, почему тесты без диска. Ответы — из уроков курса, и это лучший итог проекта: не файл ради файла, а набор решений, которые можно объяснить.
Проверка: карта стилей
Зачем в Builder карта styles map[string]int?
Проверка: тест итоговой формулы
Как тест проверяет итоговую строку с формулой SUM?
Что вернёт D5
Прочитайте код и выберите, что он выведет:
cells := []interface{}{"Север", "A-1", 3, 199.9}
f.SetSheetRow("Отчёт", "A5", &cells)
v, _ := f.GetCellValue("Отчёт", "D5")
fmt.Println(v)Соберите слои генератора
Допишите порядок сборки, запись строки, итоговую формулу и диаграмму.
Соберите конвейер генератора
Расставьте порядок работы генератора: загрузить данные, создать сборщик, собрать отчёт, довести оформление, выгрузить в буфер, записать файл.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: итоговый проект
Курс excelize закрыт: от первого файла до генератора с тестами. Скелет с курсором базы — в уроке про экспорт из базы, кэш стилей и виды оформления — в уроке про стили ячеек, ссылки на диапазоны для диаграммы — в уроке про диаграммы, тесты чтения обратно и golden-снимки — в уроке про тесты генерации, свойства и печать — в уроке про метаданные и печать, а большие выгрузки — в уроке про StreamWriter. Домашнее задание — собрать проект по плану урока, прогнать тесты и добавить одно расширение из кейса: эндпоинт, конфиг колонок или потоковую запись. Генерация отчетов go встречается почти в каждой бэкенд-задаче, и теперь у вас есть карта, проверенная файлом, который открывается без сюрпризов.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Проект собирается по слоям: типы и Builder, стили, шапка, строки, итоги, доводка, тесты.
- Карта стилей даёт один NewStyle на вид оформления: таблица стилей файла остаётся компактной.
- SetSheetRow пишет строку одним вызовом, зебра применяется диапазоном по чётным строкам.
- Итоговая строка получает формулу SUM: значение считает Excel, тест сверяет текст формулы.
- Polish добавляет диаграмму, DocProps, область печати и колонтитулы по правилам своих уроков.
- Тесты чтения обратно и golden-снимок проходят в CI: пакет можно менять без страха за файл.
Домашнее задание
Соберите проект по плану урока: пакет report с Row, Builder, Build, Polish и Close, карта из трёх стилей, итоговая формула SUM, диаграмма по регионам, свойства и печать. Напишите тест чтения обратно на шапку, крайние строки и текст формулы, снимите golden-снимок значений. Затем добавьте одно расширение из кейса: REST-эндпоинт над пакетом, конфигурацию колонок из файла или переход записи на StreamWriter с сохранением всех тестов зелёными.
Личные заметкиПусто
Вопросы и ответы по уроку
Проект принимает срез строк — а если данных миллион?
Переведите вход на курсор базы из урока про экспорт и запись на StreamWriter из урока про потоковую запись: методы Builder сохранят форму, а память перестанет расти с числом строк. Тесты на маленьких данных остаются без изменений.
Почему итог формулой, а не числом?
Формула остаётся живой: пользователь отфильтрует или допишет строку, и итог пересчитается при открытии. Число в ячейке быстрее для машины, но мёртвое для отчёта; если нужна и скорость, и живость — числа в данных, формула в итогах.
Как показать проект в портфолио?
Оформите пакет с README: пример данных, скриншот файла, раздел решений — почему стили в карте, почему формула, почему тесты без диска — и CI с тестами. Решения продают проект лучше кода: на собеседовании спрашивают именно про них.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Фоновые задачи в Go: планировщик на robfig/cron
Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.
- Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
- Читать и писать cron-выражения: пять полей, спецсимволы, @daily
- Управлять жизненным циклом: Start, Stop, Entry, Remove
- Держать часовые пояса и секундные расписания под контролем