Чтение своего файла — это проверка того, что вы записали: слаги колонок известны, типы гарантированы, границы посчитаны. Чтение чужого файла устроено иначе: лист — это договор, который составил не вы, и каждый импорт начинается с разведки. Первый шаг разведки — шапка: заголовки колонок скажут, что в файле лежит, а их порядок ничего не гарантирует. Отсюда главный принцип урока: маппим колонки по заголовкам, а не по индексам, потому что индексы живут ровно до первого файла, где поставщик вставил колонку посередине. Второй принцип: всё, что вынимается из ячеек, — строки, и приведение к числам и датам — ваша работа с обработкой ошибок. Третий принцип: битая строка — это не повод ронять импорт, а запись в журнал с номером строки, который пользователь сопоставит со своим файлом. Эти три принципа мы и разберём по очереди, с кодом и таблицей типовых поломок.
Excelize импорт: маппинг по заголовкам#
Схема маппинга одна и та же в любом импорте: прочитать первую строку, построить карту заголовок — индекс колонки, дальше разбирать строки по именам колонок. Заголовки перед построением карты чистят TrimSpace: лишний пробел в конце названия колонки — классическая причина, почему колонка «не находится». Для доступа к ячейке пишется маленький хелпер: он принимает строку, имя колонки и возвращает значение или пустоту, если колонки нет либо она пуста. Хелпер заодно решает вторую проблему — недозаполненные строки: срез из GetRows может быть короче, чем колонок в шапке, и прямое обращение по индексу паникует. Структуры-приёмники описываются под ваш домен: артикул строкой, количество целым, цена дробным, дата поставки временем. Так excelize импорт превращается из угадывания индексов в чтение договора: колонка ищется по имени, а сдвиги в файле перестают быть катастрофой.
Маппинг прайса в структуры
GOrows, 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 шт», не парсится в число, но сообщить об этом стоит человеку, а не исключению.
Парсинг с накоплением ошибок
GOtype 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 по диапазону данных. Спорная сторона осталась в строгости: одна часть команды выступает за жёсткий импорт, где файл с ошибками не принимается целиком, вторая — за толерантный, где строки с ошибками пропускаются, а журнал уходит поставщику. Победил второй режим по умолчанию и первый — по кнопке: обе ветки выросли из одного и того же парсера.
Проверка: почему по заголовкам
Почему колонки чужого файла маппятся по заголовкам, а не по индексам?
Проверка: цена со форматом
В колонке цены значение «1 234,50». Что сделать перед strconv.ParseFloat?
Что вернёт Atoi?
Прочитайте код и выберите, что он выведет:
n, err := strconv.Atoi("1 200")
fmt.Println(n, err)Соберите ядро импорта
Допишите чтение листа, парсинг числа, даты и чистку значения.
Соберите порядок импорта
Расставьте строки в порядке выполнения: прочитать лист, построить карту заголовков, пойти по строкам данных, распарсить число, накопить ошибку, сохранить результат.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: импорт
Импорт закрыт: маппинг по заголовкам, парсинг с чисткой формата, журнал ошибок с номерами строк и итератор для больших файлов. Круг замкнулся — вы умеете возить данные в обе стороны: экспорт из базы собирал файл по слоям, этот урок возвращает его в структуры, а потоковое чтение обслуживает оба маршрута на больших объёмах. Основы чтения ячеек и приведение типов лежат в уроке про чтение ячеек, кэш формул — в уроке про формулы, а коллекция граблей чужих файлов пополняет урок про типичные ошибки. Проверить свой импорт тестами и golden-файлами можно по уроку про тесты генерации. Домашнее задание — взять настоящий чужой файл и написать для него импорт с журналом ошибок.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Колонки чужого файла маппятся по заголовкам: карта имя — индекс строится по первой строке.
- len(row) короче ширины листа: перед индексом проверяют длину, за границей хелпер возвращает пустоту.
- Числа парсятся strconv после чистки формата, даты — time.Parse с лэйаутом колонки.
- Ошибки копятся с номерами строк Excel: индекс данных плюс два при шапке в первой строке.
- В свежем файле на месте итога — текст формулы: кэш значений появляется только после Excel.
- Большие файлы читаются итератором Rows, а результат пишется в базу по ходу разбора.
Домашнее задание
Возьмите настоящий чужой файл — прайс, ведомость или выгрузку: постройте импорт по схеме урока с картой заголовков, хелпером доступа к ячейкам, парсингом strconv и time и журналом ошибок с номерами строк. Проверьте его на трёх вариантах файла: нормальном, со вставленной колонкой и с битыми значениями — и убедитесь, что паники нет, ошибки собрались списком, а данные дошли до структур. Для большого файла перепишите чтение на итератор Rows и сравните память.
Личные заметкиПусто
Вопросы и ответы по уроку
Импорт падает с паникой на некоторых строках — где искать?
Почти наверняка обращение по индексу за пределами среза: строка короче шапки, потому что пустой хвост ячеек в срез не попадает. Проверяйте len(row) перед индексом или читайте значения через хелпер, который возвращает пустоту за границами среза.
Почему вместо цены импорт читает строку с равенством?
В этой ячейке формула, а файл собран без Excel и кэша значений не имеет. Проверьте GetCellFormula, посчитайте итог в Go по диапазону данных или договоритесь о пересохранённых файлах, где Excel записал вычисленные значения.
Как быть с файлами на сотни тысяч строк?
Читайте итератором f.Rows из урока про потоковое чтение: цикл Next и Columns держит в памяти только текущую строку, Close ставится через defer. Обработанные строки пишите в базу пакетами по ходу разбора, не копя срез на весь файл.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Фоновые задачи в Go: планировщик на robfig/cron
Курс по cron в Go: robfig/cron v3, синтаксис выражений, пояса, wrappers, конкурентность и graceful shutdown. 25 уроков с примерами и проектом.
- Собирать планировщик на robfig/cron v3 и запускать задачи по расписанию
- Читать и писать cron-выражения: пять полей, спецсимволы, @daily
- Управлять жизненным циклом: Start, Stop, Entry, Remove
- Держать часовые пояса и секундные расписания под контролем