Парсинг и генерация XML и CSV в Go: чек-лист перед продакшеном
Стандартная библиотека Go закрывает XML и CSV без внешних зависимостей, но в проде всплывают кодировки, неэкранированные разделители, гигабайтные файлы и молчаливые потери данных. Собираем чек-лист, который спасёт от типичных сюрпризов.

Почему стандартной библиотеки недостаточно «из коробки»
Пакеты encoding/xml и encoding/csv в Go 1.22+ покрывают 90 % задач по парсингу и генерации. Проблема — оставшиеся 10 %, которые обычно стреляют уже в проде: файл в Windows-1251 вместо UTF-8, двухгигабайтный выгрузочный XML от контрагента, BOM-символ в начале CSV, поле с переносом строки, ломающее наивный strings.Split. Ниже — конкретные приёмы и проверки, которые стоит пройти до деплоя.
XML: что ломается и как чинить
Потоковый парсинг вместо полной загрузки
xml.Unmarshal читает весь документ в память. Для файлов больше нескольких десятков мегабайт это путь к OOM. Потоковый xml.Decoder читает токен за токеном и позволяет обрабатывать вложенные элементы поштучно.
// Go 1.22+
package main
import (
"encoding/xml"
"fmt"
"os"
)
// Order — один элемент выгрузки
type Order struct {
ID string `xml:"id"`
Total float64 `xml:"total"`
}
func main() {
file, err := os.Open("orders.xml")
if err != nil {
fmt.Println("не удалось открыть файл:", err)
return
}
defer file.Close()
decoder := xml.NewDecoder(file)
for {
tok, err := decoder.Token()
if err != nil {
break // io.EOF или реальная ошибка
}
switch se := tok.(type) {
case xml.StartElement:
if se.Name.Local == "order" {
var o Order
if err := decoder.DecodeElement(&o, &se); err != nil {
fmt.Println("битый элемент, пропускаем:", err)
continue
}
fmt.Printf("заказ %s на сумму %.2f\n", o.ID, o.Total)
}
}
}
}
Проверка: запустите на файле в 500 МБ и убедитесь, что RSS-потребление процесса остаётся в пределах десятков мегабайт, а не повторяет размер файла.
Кодировки, отличные от UTF-8
xml.Decoder умеет работать только с UTF-8 и некоторыми кодировками, если задать CharsetReader. Без этого файл в Windows-1251 (частая история с 1С-выгрузками) вернёт ошибку xml: encoding "windows-1251" declared but Decoder.CharsetReader is nil.
import "golang.org/x/text/encoding/charmap"
decoder := xml.NewDecoder(file)
decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "windows-1251":
return charmap.Windows1251.NewDecoder().Reader(input), nil
default:
return nil, fmt.Errorf("неизвестная кодировка: %s", charset)
}
}
Пакет golang.org/x/text — полуофициальный, поддерживается командой Go. Точный API смотрите в документации модуля для вашей версии.
Пространства имён и самозакрывающиеся теги
При генерации XML через xml.Marshal пустые элементы превращаются в <tag></tag>, а не в <tag/>. Некоторые принимающие системы (например, старые SOAP-шлюзы) такое отвергают. Если это ваш случай, постобработка строки или шаблон text/template — проще, чем кастомный Marshaler.
Не доверяйте xml.Unmarshal молча: он не валидирует по XSD. Если контрагент прислал XML, который «парсится без ошибок», это ещё не значит, что данные корректны. Валидацию по схеме стандартная библиотека Go не поддерживает — используйте внешние инструменты или проверяйте бизнес-правила вручную.
CSV: что ломается и как чинить
BOM, разделители, переносы внутри полей
Три классических сюрприза при приёме CSV извне:
- BOM (Byte Order Mark) — Excel на Windows добавляет
\xEF\xBB\xBFв начало UTF-8 файла. Первое поле первой строки получает невидимый мусор. Лечение: срезать первые три байта, если они совпадают с BOM, до передачи вcsv.Reader. - Точка с запятой вместо запятой — выгрузки из российских систем часто используют
;. Задайтеreader.Comma = ';'. - Перенос строки внутри поля —
csv.Readerсправляется, если поле в кавычках. Наивныйbufio.Scanner+strings.Split— нет. Не изобретайте свой парсер.
// Go 1.22+
package main
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"os"
)
func main() {
raw, err := os.ReadFile("report.csv")
if err != nil {
fmt.Println(err)
return
}
// Убираем BOM, если есть
raw = bytes.TrimPrefix(raw, []byte("\xEF\xBB\xBF"))
reader := csv.NewReader(bytes.NewReader(raw))
reader.Comma = ';'
reader.LazyQuotes = true // терпимость к «кривым» кавычкам
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
fmt.Println("ошибка строки:", err)
continue
}
fmt.Println(record)
}
}
Проверка: подайте файл, в котором одно поле содержит перенос строки и точку с запятой одновременно. Если парсер выдал лишнюю строку — значит, кавычки потерялись.
Генерация CSV: кавычки и CRLF
csv.Writer автоматически оборачивает поле в кавычки, если внутри есть запятая, кавычка или перенос. Но если вы передаёте Comma = ';', он будет реагировать на точку с запятой, а не на запятую — проверьте это на тестовых данных. Также csv.Writer по умолчанию использует \n как терминатор строки. Если получатель — Windows-система, которая ждёт \r\n, задайте writer.UseCRLF = true.
Сравнение подходов: что когда выбрать
| Критерий | xml.Unmarshal / csv.ReadAll | xml.Decoder (потоковый) / csv.Reader (построчный) |
|---|---|---|
| Память | Пропорциональна размеру файла | Константная (один элемент / одна строка) |
| Удобство | Одна строка кода, результат — готовая структура | Цикл, ручная обработка каждого токена или записи |
| Обработка ошибок | Всё или ничего: первая ошибка — весь файл отброшен | Можно пропустить битый элемент, залогировать, продолжить |
| Файлы > 100 МБ | Рискованно | Штатный режим |
| Частичная обработка | Нет — весь документ в памяти | Да — можно остановиться после N элементов |
Правило простое: если файл приходит от внешней системы и его размер не контролируется вами — всегда потоковый режим.
Чек-лист перед продакшеном
- Кодировка — убедитесь, что парсер обрабатывает реальную кодировку входящих данных, а не только UTF-8.
- BOM — срезайте перед парсингом CSV. Для XML
Decoderсправляется сам. - Разделитель CSV — явно задан в конфиге или определяется из первой строки, а не захардкожен как запятая.
- Потоковый режим — для любого файла, размер которого вы не контролируете.
- Ограничение размера — оборачивайте входящий
io.Readerвio.LimitReader, если файл приходит по сети. Без лимита один запрос может съесть всю память. - Валидация данных — парсинг без ошибок не означает корректности. Проверяйте обязательные поля, диапазоны, форматы дат после десериализации.
- Логирование битых записей — не глотайте ошибки. Записывайте номер строки (CSV) или путь к элементу (XML) и текст ошибки.
- Тест на «грязных» данных — подготовьте файл-«монстр»: BOM, перенос в поле, пустые строки, кириллица в именах тегов, дублирующиеся атрибуты. Прогоните через парсер до деплоя.
- CRLF при генерации — проверьте, какой терминатор ждёт получатель.
csv.Writer.UseCRLFи постобработка для XML. - XSD / JSON Schema — если контрагент предоставляет схему, валидируйте по ней. Стандартная библиотека Go не умеет XSD-валидацию — это отдельный инструмент или сервис.
Сноска: BOM (Byte Order Mark) — невидимая последовательность байт в начале файла, указывающая на кодировку и порядок байт. В UTF-8 она необязательна и обычно вредна, но Excel для Windows добавляет её по умолчанию при сохранении в CSV.
Когда стандартной библиотеки действительно мало
Два случая, в которых стоит смотреть за пределы encoding/xml и encoding/csv:
- XSD-валидация XML — в экосистеме Go нет зрелого чисто-Go валидатора по XSD. Распространённое решение — вызов
xmllintчерезexec.Commandили использование CGo-обёрток над libxml2. Оба варианта добавляют зависимость на системном уровне. - Очень большие CSV с типизацией — если CSV по сути таблица на десятки миллионов строк и нужна аналитика, а не построчная обработка, проще загрузить данные в DuckDB или ClickHouse, чем парсить в Go построчно.
Что делать дальше
Пройдите чек-лист на реальных файлах от ваших контрагентов, а не на синтетике. Заведите в репозитории каталог testdata/ с «грязными» примерами и гоняйте по ним тесты при каждом CI-прогоне. Замерьте потребление памяти на самом большом файле, который приходил за последний месяц — runtime.ReadMemStats или внешний профилировщик покажут, держит ли потоковый режим нагрузку. Если контрагент меняет формат без предупреждения (а он меняет), добавьте алерт на рост числа ошибок парсинга — это дешевле, чем узнать о проблеме от бухгалтерии через неделю.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Пакет encoding/xml — официальная документация Go, Пакет encoding/csv — официальная документация Go
Частые вопросы
Как определить кодировку входящего файла автоматически?
csv.Reader падает с ошибкой «wrong number of fields» — что делать?
Можно ли парсить XML с невалидными символами?
Что если у меня Go версии ниже 1.22 — код из статьи сработает?
Как безопасно принимать XML по HTTP, чтобы не получить OOM или XXE?
Как не потерять данные при генерации CSV с кириллицей для Excel?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.