golang

Динамическая функция для генерации excel файла на Golang

  • пятница, 10 мая 2024 г. в 00:00:08
https://habr.com/ru/articles/813335/

Привет! Я Сино, бэкенд разрабочик на языке Golang. Работаю 1,5 года в банковской сфере.

Так как я работаю в команде B2B, из-за этого часто приходится работать с отчётностью, платежами и файлами.

В связи с тем, что часто приходится генерировать отчёт в разных файловых форматах, таких как xlsx, pdf, doc и т.п., мне приходится знать все тонкости работы с файлами. Самая частый формат из перечисленного это - excel формат (xlsx), так как, основная работа B2B - это работа с мерчантами (мерчант - это юр. лицо имеющее безналичную оплату...) и разными видами отчётности для них. В связи с этим, нужно было найти удобный пакет с множественными возможностями. Для работы c excel файлом есть удобный и расширенный пакет на Go - excelize/v2, на котором я остановился (пакетов особо не много). Данный пакет удобный и в полне может охватить все наши требования к excel файлу. Для более маштабного представления о возможностях пакета, можете ознакомиться по ссылке:

Документация пакета Excelize

Теперь немного о своей проблеме: Так как язык Go строго типизированный и при создании функции, для разделение какой-либо логики, нам нужно в сигнатуру (Сигнатура — это объявление функции или метода, а также его параметры) функции указать особый тип принимающих параметров. Например:

func TransactionExelGenerator(transactions []*types.Transactions) (*bytes.Buffer, error) {

//логика функции

}

Данная функция принимает массив структуры Transactions и как было подмечено в эту функцию можно передать только данные имеющие тип, указанный в сигнатуре функции.

Каждый раз, под каждую структуру приходилось создавать новую функцию, что уже не хорошо.

Было бы логично возмущатся, что можно было использовать дженерики или же пустой интерфейс, чтобы принимать любой тип, но у нас продукт был запущен на проде с версией golang-а 1.17 (из-за множество зависимостей и интеграций, возможности обновить версию языка на сервере не было), где нет дженериков (хотя первое решение было реализовано с помощью дженериков), а пустой интерфейс заставляет каждый раз подбирать ожидаемый тип аргументов и определять свитч кейсом (switch case). В итоге остановились на интерфейсе, но не пустой интерфейс. Давайте более подробно пройдёмся:

Мы знаем, что интерфейс - это некая абстракция предсказывающая поведение типов или проще сказать, некий договор определящая поведение типов удовлетворящие методы данного интерфейса... Согласно этой трактовки нам нужно определять методы, которые помогают создавать наш файл и поэтому я разбил структуру своего excel файла на следующие части, которые в основном у нас используются:

  • Title

  • Title rows

  • Header

  • Header rows

    Это примерно выглядит вот так:

Теперь после того как мы определились со структурой своего файла, постараемся реализовать наш интерфейс, который будет удовлетворять наши требования к файлу:

Создаём файл main.go и внутри создаём функцию main, которая является точкой входа в приложение и запускает его:

func main() {    

//function body

}

После этого нам нужно создать свой интерфейс сверху функции main чтобы он стал глобальным (более подробно советую прочитать про область видимости):

type Excel interface {

}

func main() {    

//function body

}

Для основных частей нашего excel файла создаём методы, с которыми в дальнейшем будем работать:

// Excel представляет интерфейс для работы с данными Excel
type Excel interface {
	Header() []string
	Title() []string
	TitleRows() []interface{}
	HeaderRows() [][]interface{}
}

func main() {    

//function body

}

После того как мы заполнили наш интерфейс методами, можем приступить к созданию функции, которая генерирует для нас excel файл. Вы можете сигнатуру под себя подстраивать. В качестве входных данных передадим наш интерфейс, чтобы принимать любую структуру, которая имплементирует наш интерфейс и возвращаем байты храняшейся в буфере:

func ExcelGenerator(data Excel) (*bytes.Buffer, error) {
//Создаём новый файл
file := excelize.NewFile()
// Название листа
sheetName := "Sheet1"

// Получение заголовков
header := data.Header()

// Получение строк заголовков
rows := data.HeaderRows()

// Получение заголовков подзаголовков
titles := data.Title()

// Получение строк подзаголовков
titleRows := data.TitleRows()

// Создание заголовков
for i, h := range titles {
    // Получение имени столбца по его номеру (A, B, C, ...)
    colName, _ := excelize.ColumnNumberToName(1)
    // Установка значения заголовка в ячейку
    file.SetCellValue(sheetName, colName+strconv.Itoa(i+1), h)
}

// Заполнение заголовков
for i, h := range titleRows {
    // Получение имени столбца по его номеру (A, B, C, ...)
    colName, _ := excelize.ColumnNumberToName(2)
    // Установка значения заголовка в ячейку
    file.SetCellValue(sheetName, colName+strconv.Itoa(i+1), h)
}

// Создание заголовков колонок
for i, h := range header {
    // Получение имени столбца по его номеру (A, B, C, ...)
    colName, _ := excelize.ColumnNumberToName(i + 1)
    // Установка значения заголовка колонки в ячейку
    file.SetCellValue(sheetName, colName+"7", h)
}

// Заполнение колонок
for r, row := range rows {
    for c, val := range row {
        // Получение имени столбца по его номеру (A, B, C, ...)
        colName, _ := excelize.ColumnNumberToName(c + 1)
        // Установка значения в соответствующую ячейку
        file.SetCellValue(sheetName, colName+strconv.Itoa(r+8), val)
    }
}

// Запись файла в буфер
f, _ := file.WriteToBuffer()

// Запись файла на диск (опционально)
ioutil.WriteFile("test.xlsx", f.Bytes(), 0644)

// Возвращаем буфер и ошибку (если есть)
return f, nil

}

Данная функция вне области видимости функции main. Теперь нужно имитировать реальный кейс с данными. Создаём одну тестовую структуру и заполняем её данными:

// Excel представляет интерфейс для работы с данными Excel
type Excel interface {
	Header() []string
	Title() []string
	TitleRows() []interface{}
	HeaderRows() [][]interface{}
}

//Тестовая структура сотрудников
type TestData struct {
	TestData []struct {
		Id       int    `json:"id"`
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Position string `json:"position"`
	}
}

func main() {    
//Заполняем тестовые данные
 testData := TestData{
TestData: []struct {
  Id       int    `json:"id"`
  Name     string `json:"name"`
  Age      int    `json:"age"`
  Position string `json:"position"`
}{
  {Id: 1, Name: "John", Age: 30, Position: "Developer"},
  {Id: 2, Name: "Alice", Age: 25, Position: "Designer"},
  {Id: 3, Name: "Bob", Age: 35, Position: "Manager"},
  },
}
}

func ExcelGenerator(data Excel) (*bytes.Buffer, error) {
  ...

После того, как мы создали тестовую структуру и заполнили данными, попробуем эти данные передать нашей функции ExcelGenerator, но как вы думаете, что мы получим?

// Excel представляет интерфейс для работы с данными Excel
type Excel interface {
	Header() []string
	Title() []string
	TitleRows() []interface{}
	HeaderRows() [][]interface{}
}

//Тестовая структура сотрудников
type TestData struct {
	TestData []struct {
		Id       int    `json:"id"`
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Position string `json:"position"`
	}
}

func main() {    
//Заполняем тестовые данные
 testData := TestData{
TestData: []struct {
  Id       int    `json:"id"`
  Name     string `json:"name"`
  Age      int    `json:"age"`
  Position string `json:"position"`
}{
  {Id: 1, Name: "John", Age: 30, Position: "Developer"},
  {Id: 2, Name: "Alice", Age: 25, Position: "Designer"},
  {Id: 3, Name: "Bob", Age: 35, Position: "Manager"},
  },
}
  //Передача данных для генерации excel файла
  file, err := ExcelGenerator(&testData)
	if err != nil {
		log.Println(err)
		return
	}
}

func ExcelGenerator(data Excel) (*bytes.Buffer, error) {
  ...
Ответ

У нас компиляция упадёт с принудительной остановкой(exit code 1):

.\main.go:33:30: cannot use &testData (value of type *TestData) as Excel value in argument to ExcelGenerator: *TestData does not implement Excel (missing method Header)

Compilation finished with exit code 1

Всё потому, что наша тестовая структура не имплементирует методы нашего интерфейса и поэтому не можем её передать в функцию. Для этого мы должны к нашей тестовой структуре привязать методы, которые есть у интерфейса, чтобы она могла удовлетворить наш интерфейс и тогда мы сможем передать нашу структуру как аргумент функции:

// Excel представляет интерфейс для работы с данными Excel
type Excel interface {
	Header() []string
	Title() []string
	TitleRows() []interface{}
	HeaderRows() [][]interface{}
}

//Тестовая структура сотрудников
type TestData struct {
	TestData []struct {
		Id       int    `json:"id"`
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Position string `json:"position"`
	}
}

//Имплементация методов интерфейса
func (td *TestData) Title() []string {
	return []string{"Отчёт по сотрудникам"}
}

func (td *TestData) TitleRows() []interface{} {
	return []interface{}{""}
}

func (td *TestData) Header() []string {
	return []string{"№", "Название", "Возраст", "Должность"}
}

func (td *TestData) HeaderRows() [][]interface{} {
	var row [][]interface{}
	for _, value := range td.TestData {
		r := []interface{}{
			value.Id,
			value.Name,
			value.Age,
			value.Position,
		}
		row = append(row, r)
	}
	return row
}


func main() {    

 testData := TestData{
TestData: []struct {
  Id       int    `json:"id"`
  Name     string `json:"name"`
  Age      int    `json:"age"`
  Position string `json:"position"`
}{
  {Id: 1, Name: "John", Age: 30, Position: "Developer"},
  {Id: 2, Name: "Alice", Age: 25, Position: "Designer"},
  {Id: 3, Name: "Bob", Age: 35, Position: "Manager"},
  },
}
  file, err := ExcelGenerator(&testData)
    if err != nil {
    log.Println(err)
      return
  }
  
}

func ExcelGenerator(data Excel) (*bytes.Buffer, error) {
  ...

Мы привязали методы нашего интерфейса к нашей структуре и теперь можем запустить наше приложение, но перед этим давайте добавим пару строчек кода, чтобы сохранить наш файл на локальном, чтобы удобнее было тестировать:

// Excel представляет интерфейс для работы с данными Excel
type Excel interface {
	Header() []string
	Title() []string
	TitleRows() []interface{}
	HeaderRows() [][]interface{}
}

//Тестовая структура сотрудников
type TestData struct {
	TestData []struct {
		Id       int    `json:"id"`
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Position string `json:"position"`
	}
}

//Имплементация методов интерфейса
func (td *TestData) Title() []string {
	return []string{"Отчёт по сотрудникам"}
}

func (td *TestData) TitleRows() []interface{} {
	return []interface{}{""}
}

func (td *TestData) Header() []string {
	return []string{"№", "Название", "Возраст", "Должность"}
}

func (td *TestData) HeaderRows() [][]interface{} {
	var row [][]interface{}
	for _, value := range td.TestData {
		r := []interface{}{
			value.Id,
			value.Name,
			value.Age,
			value.Position,
		}
		row = append(row, r)
	}
	return row
}


func main() {    

 testData := TestData{
TestData: []struct {
  Id       int    `json:"id"`
  Name     string `json:"name"`
  Age      int    `json:"age"`
  Position string `json:"position"`
}{
  {Id: 1, Name: "John", Age: 30, Position: "Developer"},
  {Id: 2, Name: "Alice", Age: 25, Position: "Designer"},
  {Id: 3, Name: "Bob", Age: 35, Position: "Manager"},
  },
}
  file, err := ExcelGenerator(&testData)
    if err != nil {
    log.Println(err)
      return
  }
  
  //Создаём пустой файл формата excel
  data, err := os.Create("output.xlsx")
	if err != nil {
		fmt.Println("Ошибка при открытии файла:", err)
		return
	}
	defer data.Close()

	// Записываем содержимое буфера в файл
	_, err = file.WriteTo(data)
	if err != nil {
		fmt.Println("Ошибка при записи в файл:", err)
		return

	}
}

func ExcelGenerator(data Excel) (*bytes.Buffer, error) {
  ...

После запуска мы получим наш файл в корневой директории проекта и посмотрим его содержимое:

Как мы видим сгенерировался excel файл с ожидаемым шаблоном и так можно сколько угодно варинтов подбирать и всячески под себя настраивать функцию. Можно по разному оформлять файл исходя от ваших требований. Надеюсь статья была полезной.