Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 25 из 25 ~24 мин +20 XP

Итоговый проект: генератор отчётов xlsx

Excelize проект в финале курса — генератор отчётов xlsx, который собирает всё пройденное в один файл: отчёт продаж со стилями, зеброй, итоговой формулой, диаграммой, свойствами документа, настройками печати и тестами. Это обзорная сборка: каркас и решения на месте, а детали каждого шага живут в своих уроках, и по ходу будут ссылки. Соберите пакет, прогоните тесты и получите шаблон, который переносится в рабочий сервис почти без правок.

Содержание урока · 9 разделов

Проект ставит задачу, а не повторяет уроки: на вход приходит срез продаж, на выходе — файл, который не стыдно отдать заказчику. Внутри сойдутся все слои курса: типы и данные из уроков про запись и чтение, стили и форматы, итоги формулой, диаграмма, свойства и печать, тесты чтения обратно. Формат урока — обзорная сборка: мы пройдём по структуре пакета и ключевым решениям, а построчные детали остаются в своих уроках, и на каждом шаге будет ссылка. Три критерия готовности: файл собирается командой, тесты проверяют содержимое через чтение обратно, и каждый новый вид оформления появляется в коде одним местом, а не разбросом вызовов. Если после сборки захочется продолжить — в кейсе в конце список расширений: REST-эндпоинт, потоковая запись для больших объёмов и конфигурация колонок снаружи.

Excelize проект: требования и план работ#

Задание проекта: функция, которая принимает срез продаж и возвращает xlsx с шапкой на заливке, зеброй по чётным строкам, заданными ширинами колонок, закреплённой шапкой, автофильтром, итоговой строкой с формулой SUM, столбчатой диаграммой по регионам, свойствами документа и настройками печати. Генерация отчетов go такого масштаба — это не один файл на тысячу строк, а пакет с ясными границами: типы данных, сборщик, доводка, тесты. План работ выглядит так: описать типы и каркас сборщика, собрать стили и шапку, записать строки с зеброй, поставить итоги, добавить диаграмму и доводку, покрыть тестами. Урок — обзорная сборка: каркас и решения на месте, а подробности каждого шага живут в своих уроках курса, и по ходу будут ссылки. Такой формат честнее построчного туториала: вы видите, как части соединяются, и сами дописываете детали — это и есть ежедневная работа генератора отчётов.

Каркас пакета report

GO
package 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 из урока про потоковую запись.

Слои сборки: стили, строки, итоги

GO
func (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 собирает слои в порядке урока про экспорт, и каждая ошибка возвращается наверх с местом возникновения.

Доводка: диаграмма, свойства и печать#

Второй этап — Polish: слой удобств, который превращает таблицу в отчёт. Диаграмма добавляется AddChart: столбчатый тип по регионам, категории из колонки A, значения из колонки D, подпись сверху — синтаксис ссылок и серий разобран в уроке про диаграммы. Свойства документа задаются DocProps с названием отчёта и периодом, область печати — по фактической границе данных, первая строка повторяется на каждой странице, колонтитул показывает название слева и номера страниц справа — всё это из урока про метаданные и печать. Ширины колонок и закрепление уже стоят с первого этапа, автофильтр включается по шапке. Порядок доводки тот же, что и в уроках: сначала данные и оформление, затем удобства, затем сохранение. Отдельно отметим компоновку: диаграмма ставится правее данных, чтобы печать области не втаскивала её в каждую страницу, а заголовок отчёта живёт в свойствах, а не в объединённой ячейке — за это правило кто-то поспорит, и это хороший спор для ревью.

Polish: диаграмма и печатный вид

GO
func (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. Третий — таблица кейсов на данных: пустой срез, одна строка, отрицательные количества; грани данных ломают генераторы чаще, чем средние значения. На тесты проекта стоит потратить столько же времени, сколько на сборку: пакет, который можно спокойно менять, переиспользуют, а пакет без тестов переписывают с нуля при первом же новом требовании.

Тест генератора

GO
func 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 и зебра диапазономчтение первой и последней строки
Итогиподпись и формула SUMGetCellFormula и стиль итоговой строки
Доводкадиаграмма, DocProps, печатьGetDocProps и ручной предпросмотр
Тестычтение обратно и goldengo 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)

Соберите слои генератора

Допишите порядок сборки, запись строки, итоговую формулу и диаграмму.

func (b *Builder) Build() error { if err := b.initStyles(); err != nil { return err } if err := b.(); err != nil { return err } return b.addTotals() } b.f.("Отчёт", fmt.Sprintf("A%d", row), &cells) return b.f.("Отчёт", cell, fmt.Sprintf("SUM(D2:D%d)", last)) _, err := b.f.("Отчёт", "F2", chart)

Соберите конвейер генератора

Расставьте порядок работы генератора: загрузить данные, создать сборщик, собрать отчёт, довести оформление, выгрузить в буфер, записать файл.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: итоговый проект

Термин

Builder

нажмите, чтобы перевернуть
Определение

Сборщик отчёта: держит книгу, строки данных и карту стилей. Методы по слоям: Build, Polish, Close.

1 / 6

Курс 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 с сохранением всех тестов зелёными.

Личные заметкиПусто
Хранятся только в вашем браузере (Excelize · Итоговый проект: генератор отчётов xlsx)

Вопросы и ответы по уроку

Проект принимает срез строк — а если данных миллион?

Переведите вход на курсор базы из урока про экспорт и запись на StreamWriter из урока про потоковую запись: методы Builder сохранят форму, а память перестанет расти с числом строк. Тесты на маленьких данных остаются без изменений.

Почему итог формулой, а не числом?

Формула остаётся живой: пользователь отфильтрует или допишет строку, и итог пересчитается при открытии. Число в ячейке быстрее для машины, но мёртвое для отчёта; если нужна и скорость, и живость — числа в данных, формула в итогах.

Как показать проект в портфолио?

Оформите пакет с README: пример данных, скриншот файла, раздел решений — почему стили в карте, почему формула, почему тесты без диска — и CI с тестами. Решения продают проект лучше кода: на собеседовании спрашивают именно про них.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Cron25 уроков~9 чпрактика25 уроков уже доступны

Фоновые задачи в Go: планировщик на robfig/cron

Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.

  • Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
  • Читать и писать cron-выражения: пять полей, спецсимволы, @daily
  • Управлять жизненным циклом: Start, Stop, Entry, Remove
  • Держать часовые пояса и секундные расписания под контролем