---
title: "Импорт: таблица в структуры Go"
description: "Excelize импорт: парсинг excel go таблицы в структуры — маппинг колонок по заголовкам, числа через strconv, валидация строк и журнал ошибок по строкам."
course: "Excel в Go: чтение и генерация xlsx на excelize"
url: "https://golangmaster.ru/courses/excelize/lessons/20-import-v-struktury/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Импорт: таблица в структуры Go

*Курс «Excel в Go: чтение и генерация xlsx на excelize», урок 20 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/excelize/lessons/20-import-v-struktury/*

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

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

### Excelize импорт: маппинг по заголовкам

Схема маппинга одна и та же в любом импорте: прочитать первую строку, построить карту заголовок — индекс колонки, дальше разбирать строки по именам колонок. Заголовки перед построением карты чистят TrimSpace: лишний пробел в конце названия колонки — классическая причина, почему колонка «не находится». Для доступа к ячейке пишется маленький хелпер: он принимает строку, имя колонки и возвращает значение или пустоту, если колонки нет либо она пуста. Хелпер заодно решает вторую проблему — недозаполненные строки: срез из GetRows может быть короче, чем колонок в шапке, и прямое обращение по индексу паникует. Структуры-приёмники описываются под ваш домен: артикул строкой, количество целым, цена дробным, дата поставки временем. Так excelize импорт превращается из угадывания индексов в чтение договора: колонка ищется по имени, а сдвиги в файле перестают быть катастрофой.

### Маппинг прайса в структуры

```go
rows, err := f.GetRows("Прайс")
if err != nil {
    panic(err)
}
if len(rows) == 0 {
    panic(errors.New("пустой лист"))
}

header := map[string]int{}
for i, name := range rows[0] {
    header[strings.TrimSpace(name)] = i
}

type Item struct {
    SKU   string
    Name  string
    Price float64
    Qty   int
}

pick := func(row []string, name string) string {
    idx, ok := header[name]
    if !ok || idx >= len(row) {
        return ""
    }
    return strings.TrimSpace(row[idx])
}

items := make([]Item, 0, len(rows)-1)
for _, row := range rows[1:] {
    price, _ := strconv.ParseFloat(
        strings.ReplaceAll(pick(row, "Цена"), ",", "."), 64)

    items = append(items, Item{
        SKU:   pick(row, "Артикул"),
        Name:  pick(row, "Наименование"),
        Price: price,
    })
}
```

Карта заголовков строится один раз по первой строке, хелпер pick достаёт значения по именам колонок и не паникует на недозаполненных строках. Цена приходит форматированной, поэтому запятая заменяется на точку до ParseFloat — о разбросе форматов дальше и отдельно.

> **Хвостовые пустые ячейки.** Срез строки из GetRows короче видимой ширины листа: пустой хвост в срез не попадает, а недозаполненная строка в середине файла короче шапки. Обращение row[5] без проверки длины падает с паникой на первой же строке с дырой. Проверяйте len(row) перед индексом или работайте через хелпер, который возвращает пустоту за границами среза. Если файл читается итератором из урока про потоковое чтение, скрытые фильтром строки отличайте по GetRowOpts — в чужом прайсе они встречаются чаще, чем кажется.

### Числа и даты: strconv и time

Ячейка с числом в чужом файле — это отформатированная строка: с разделителями групп «1 234», с запятой вместо точки, с валютой на конце. strconv.ParseFloat такое не съест — сначала значения приводят к машинному виду: пробелы и неразрывные пробелы убираются, запятая меняется на точку, валютные символы срезаются. Опция RawCellValue при чтении отдаёт сырые значения без наложения формата и часто экономит половину чистки. Даты разбираются time.Parse с лэйаутом конкретной колонки — «02.01.2006» для российских прайсов, и это отдельная колонка, потому что даты в разных колонках одного файла встречаются в разных форматах. Булевы значения приходят «TRUE», «1», «да» — и сводятся к bool вашими правилами. Ошибки парсинга — это данные о данных: строка, где количество читается как «10 шт», не парсится в число, но сообщить об этом стоит человеку, а не исключению.

### Парсинг с накоплением ошибок

```go
type RowError struct {
    Row int
    Msg string
}

var rowErrs []RowError

for i, row := range rows[1:] {
    excelRow := i + 2

    qty, err := strconv.Atoi(pick(row, "Количество"))
    if err != nil {
        rowErrs = append(rowErrs, RowError{
            Row: excelRow, Msg: "количество: " + pick(row, "Количество"),
        })
        continue
    }

    date, err := time.Parse("02.01.2006", pick(row, "Дата поставки"))
    if err != nil {
        rowErrs = append(rowErrs, RowError{
            Row: excelRow, Msg: "дата: " + pick(row, "Дата поставки"),
        })
        continue
    }

    items = append(items, Item{Qty: qty, Date: date})
}

fmt.Printf("импортировано %d, ошибок %d
", len(items), len(rowErrs))
```

Номер строки для журнала считается как индекс плюс два: первая строка данных — вторая в Excel. Битые значения пишутся в срез ошибок с текстом ячейки, строка пропускается, и импорт доходит до конца файла — отчёт об ошибках пользователь получает одним списком.

### Валидация: копите ошибки, не роняйте импорт

Импорт, который умирает на первой битой строке, бесполезен в бою: поставщик исправит одну колонку, присланный файл сломается на другой, и цикл переписки затянется на неделю. Рабочая модель — накопление: каждая строка проходит разбор и валидацию, ошибки собираются в срез с номером строки и текстом, а импорт доходит до конца файла. Номер строки обязателен: пользователь ищет проблему в своём Excel, и «строка 214» для него значима, а «индекс 212» — нет; помните про сдвиг, потому что шапка занимает первую строку. Ошибки стоит разделять на фатальные и предупреждения: строка без артикула — фатально, она не попадёт в базу; округлённая цена — предупреждение, строка импортируется с пометкой. Журнал ошибок отдаётся тем же слоем, что и результат: срезом в ответе API, файлом-отчётом или списком в интерфейсе. Пустые строки в конце листа — не ошибки: GetRows хвостовые пустые не отдаёт, а полностью пустые строки в середине встречайте и пропускайте явно.

### Типовые поломки импорта

| Симптом | Причина | Что делать |
| --- | --- | --- |
| Паника на строке | len(row) меньше индекса | проверять длину или хелпер-возвращатель пустоты |
| Число читается в ноль | форматированное значение с пробелами | TrimSpace, убрать пробелы, RawCellValue |
| Дата не разбирается | лэйаут другой колонки | time.Parse с известным форматом |
| Колонки съехали | поставщик вставил колонку | маппинг по заголовкам, не по индексам |
| В ячейке =SUM вместо числа | свежий файл без кэша формул | GetCellFormula или пересохранённый файл |
| Импорт умер на середине | падение на первой ошибке | копить ошибки с номерами строк |

### Большие файлы: итератор вместо GetRows

Схема маппинга не зависит от способа чтения, но способ зависит от размера. GetRows собирает весь лист в память, и на прайсе в миллион строк процесс упрётся в лимит окружения задолго до конца разбора. Для больших файлов тот же код перестраивается на итератор f.Rows из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку, Close ставится через defer, а ошибки проверяются после цикла. Вторая сторона масштаба — запись результата: обработанные строки не копятся в огромном срезе структур, а уходят в базу пакетами или в файл по ходу разбора. Проверки на маленьком файле и на большом — это разные тесты: первый ловит логику маппинга, второй ловит память, и в уроке про тесты генерации мы вернёмся к обоим. Схема же остаётся той же: разведка шапки, чтение по именам колонок, парсинг с журналом ошибок.

> **Чужие формулы и кэш.** В колонке, где вы ждёте число, может лежать формула. Файл, пересохранённый Excel, несёт кэш значений — чтение вернёт числа; файл, собранный программой без Excel, кэша не имеет, и на месте итога вы прочтёте текст с равенством. Импорт не должен угадывать: проверяйте GetCellFormula на итоговых ячейках и решайте — вычислять в Go, требовать пересохранённый файл или читать диапазон данных и считать сумму самому. Правило из урока про формулы работает и здесь: машине нужны значения.

### Чек-лист импорта

- Маппинг колонок по заголовкам: карта имя — индекс строится по первой строке.
- Заголовки и значения чистятся TrimSpace: пробелы ломают и поиск колонок, и парсинг.
- len(row) проверяется перед индексами: хвостовые пустые в срез не попадают.
- Числа — strconv после чистки формата, даты — time.Parse с лэйаутом колонки.
- Ошибки копятся с номерами строк Excel: индекс плюс два при шапке в первой строке.
- Большие файлы читаются итератором Rows, а результат пишется по ходу разбора.

> **Парсинг живёт и в песочнице.** Файл в песочнице не открыть, но ядро импорта — парсинг — там тренируется в полный рост: там доступны fmt, strings, strconv, math, sort, errors и time. Возьмите срез текстовых строк с ценами «1 234,50», количествами и датами, разберите их strconv и time с накоплением ошибок по индексам и напечатайте журнал — логика та же, что в цикле импорта, а сам файл подберите локально.

Кейс про прайс, который сломался посреди квартала. Импортёр читал прайс поставщика по индексам: колонка B — артикул, D — цена, E — количество. Полгода всё работало, потом поставщик добавил колонку со скидкой между ценой и количеством, и ночной импорт начал складывать скидки в количество: числа сходились, а отчёты по остаткам разъехались на миллионы. Восстановление заняло вечер, перенос на маппинг по заголовкам — ещё два, вместе с журналом ошибок и проверками длин срезов. Второй сюрприз ждал в ценах: часть прайсов приходила из новой системы с формулами и без кэша, и вместо цены импортёр читал строку с равенством — договорились требовать пересохранённый файл, а итоги считать в Go по диапазону данных. Спорная сторона осталась в строгости: одна часть команды выступает за жёсткий импорт, где файл с ошибками не принимается целиком, вторая — за толерантный, где строки с ошибками пропускаются, а журнал уходит поставщику. Победил второй режим по умолчанию и первый — по кнопке: обе ветки выросли из одного и того же парсера.

### Проверка: почему по заголовкам

**Вопрос:** Почему колонки чужого файла маппятся по заголовкам, а не по индексам?

- ✅ Источник вставит или переставит колонку, и индексы съедут
- GetRows не отдаёт индексы колонок
- Заголовки обязательны по формату xlsx
- Так импорт работает быстрее

Индексы — деталь оформления чужого файла, и меняет её тот, кто файл делает. Маппинг по заголовкам превращает сдвиги в несовпадение имени, которое ловится проверкой, а не в тихо поехавшие данные.

### Проверка: цена со форматом

**Вопрос:** В колонке цены значение «1 234,50». Что сделать перед strconv.ParseFloat?

- ✅ Убрать пробелы и заменить запятую на точку
- ParseFloat сам поймёт русскую запись числа
- Умножить на сто и работать в копейках
- Обрезать строку по первому пробелу

strconv понимает числа только в машинной записи: без групп и с точкой-разделителем. Пробелы и неразрывные пробелы убираются, запятая меняется на точку; RawCellValue при чтении отдаёт сырые значения и сокращает чистку.

### Что вернёт Atoi?

```go
n, err := strconv.Atoi("1 200")
fmt.Println(n, err)
```

**Что выведет программа?**

- ✅ 0 и ошибку: пробел внутри числа недопустим
- 1200 и nil: пробел игнорируется
- 1200 и nil: strconv чистит пробелы сам
- 0 и nil: строка считается пустой

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

### Соберите ядро импорта

Допишите чтение листа, парсинг числа, даты и чистку значения.

```go
rows, err := f.___("Прайс")

qty, err := ___.Atoi(pick(row, "Количество"))

date, err := ___.Parse("02.01.2006", pick(row, "Дата"))

name := strings.___(pick(row, "Наименование"))
```

Подсказка: Лист читает GetRows, числа парсит strconv, даты — time.Parse, а пробелы срезает strings.TrimSpace.

Правильные ответы в пропусках: `GetRows`, `strconv`, `time`, `TrimSpace`

### Соберите порядок импорта

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

Правильный порядок строк:

1. `rows, err := f.GetRows("Прайс")`
2. `header := buildHeader(rows[0])`
3. `for i, row := range rows[1:] {`
4. `qty, err := strconv.Atoi(pick(row, "Количество"))`
5. `rowErrs = append(rowErrs, RowError{Row: i + 2, Msg: msg})`
6. `saveItems(items, rowErrs)`

### Карточки: импорт

- **Маппинг по заголовкам** — Карта имя колонки — индекс строится по первой строке. Индексы чужого файла ненадёжны: колонку вставят или переставят.
- **len(row)** — Срез строки короче ширины листа: пустой хвост в срез не попадает. Перед индексом — проверка длины или хелпер с пустотой.
- **strconv и формат** — ParseFloat и Atoi понимают только машинную запись: пробелы-разделители и запятая чистятся до парсинга.
- **RawCellValue** — Опция чтения: сырые значения без наложения формата Excel. Числа и даты приходят без человеческих разделителей.
- **Журнал ошибок** — Срез ошибок с номером строки Excel и текстом значения. Импорт доходит до конца файла, отчёт уходит одним списком.
- **Формулы без кэша** — В свежем файле на месте итога лежит текст формулы. Проверяйте GetCellFormula, считайте итоги в Go или требуйте пересохранённый файл.

Импорт закрыт: маппинг по заголовкам, парсинг с чисткой формата, журнал ошибок с номерами строк и итератор для больших файлов. Круг замкнулся — вы умеете возить данные в обе стороны: [экспорт из базы](/courses/excelize/lessons/19-eksport-iz-bd/) собирал файл по слоям, этот урок возвращает его в структуры, а [потоковое чтение](/courses/excelize/lessons/18-potokovoe-chtenie/) обслуживает оба маршрута на больших объёмах. Основы чтения ячеек и приведение типов лежат в уроке про [чтение ячеек](/courses/excelize/lessons/05-chtenie-yacheek/), кэш формул — в уроке про [формулы](/courses/excelize/lessons/11-formuly/), а коллекция граблей чужих файлов пополняет урок про [типичные ошибки](/courses/excelize/lessons/22-tipichnye-oshibki/). Проверить свой импорт тестами и golden-файлами можно по уроку про [тесты генерации](/courses/excelize/lessons/23-testy-eksporta/). Домашнее задание — взять настоящий чужой файл и написать для него импорт с журналом ошибок.

## Коротко: что запомнить

- Колонки чужого файла маппятся по заголовкам: карта имя — индекс строится по первой строке.
- len(row) короче ширины листа: перед индексом проверяют длину, за границей хелпер возвращает пустоту.
- Числа парсятся strconv после чистки формата, даты — time.Parse с лэйаутом колонки.
- Ошибки копятся с номерами строк Excel: индекс данных плюс два при шапке в первой строке.
- В свежем файле на месте итога — текст формулы: кэш значений появляется только после Excel.
- Большие файлы читаются итератором Rows, а результат пишется в базу по ходу разбора.

## Домашнее задание

Возьмите настоящий чужой файл — прайс, ведомость или выгрузку: постройте импорт по схеме урока с картой заголовков, хелпером доступа к ячейкам, парсингом strconv и time и журналом ошибок с номерами строк. Проверьте его на трёх вариантах файла: нормальном, со вставленной колонкой и с битыми значениями — и убедитесь, что паники нет, ошибки собрались списком, а данные дошли до структур. Для большого файла перепишите чтение на итератор Rows и сравните память.

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

### Импорт падает с паникой на некоторых строках — где искать?

Почти наверняка обращение по индексу за пределами среза: строка короче шапки, потому что пустой хвост ячеек в срез не попадает. Проверяйте len(row) перед индексом или читайте значения через хелпер, который возвращает пустоту за границами среза.

### Почему вместо цены импорт читает строку с равенством?

В этой ячейке формула, а файл собран без Excel и кэша значений не имеет. Проверьте GetCellFormula, посчитайте итог в Go по диапазону данных или договоритесь о пересохранённых файлах, где Excel записал вычисленные значения.

### Как быть с файлами на сотни тысяч строк?

Читайте итератором f.Rows из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку, Close ставится через defer. Обработанные строки пишите в базу пакетами по ходу разбора, не копя срез на весь файл.
