Парсинг и генерация XML и CSV в Go: делаем правильно с первого раза
Стандартная библиотека Go закрывает и XML, и CSV без внешних зависимостей — но у каждого пакета свои подводные камни. Разбираем encoding/xml и encoding/csv: структурные теги, потоковый парсинг, обработку кодировок и типичные ловушки, из-за которых данные теряются молча.

Что нужно до начала
Все примеры проверены на Go 1.22. Вам понадобится установленный Go (подойдёт и 1.21, но поведение encoding/xml с пространствами имён менялось между 1.20 и 1.21 — ниже отмечено где). Внешних пакетов не нужно: оба формата обслуживаются стандартной библиотекой.
XML: encoding/xml
Десериализация в структуру
Самый частый сценарий — получить XML-документ (ответ API, фид, выгрузка из 1С) и превратить его в Go-структуру. Ключевой механизм — структурные теги xml:"...", которые отображают элементы и атрибуты на поля.
package main
import (
"encoding/xml"
"fmt"
"strings"
)
// Пример: каталог товаров
type Catalog struct {
XMLName xml.Name `xml:"catalog"`
Products []Product `xml:"product"`
}
type Product struct {
ID int `xml:"id,attr"` // атрибут, не элемент
Name string `xml:"name"`
Price float64 `xml:"price"`
}
func main() {
data := `
Кабель USB-C
490.00
Адаптер HDMI
1120.50
`
var cat Catalog
if err := xml.Unmarshal([]byte(data), &cat); err != nil {
fmt.Println("ошибка:", err)
return
}
for _, p := range cat.Products {
fmt.Printf("%d: %s — %.2f руб.\n", p.ID, p.Name, p.Price)
}
}
Проверка: на выходе две строки с названиями и ценами. Если структура не заполнилась — скорее всего, не совпал тег xml:"..." с именем элемента в документе. Go не сообщает о непрочитанных полях — это молчаливая потеря данных, главный источник багов.
Потоковый парсинг большого файла
Файл на сотни мегабайт нельзя загружать в память целиком. xml.Decoder читает токен за токеном:
package main
import (
"encoding/xml"
"fmt"
"os"
)
func main() {
file, err := os.Open("catalog.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 el := tok.(type) {
case xml.StartElement:
if el.Name.Local == "product" {
var p Product
if err := decoder.DecodeElement(&p, &el); err != nil {
fmt.Println("ошибка элемента:", err)
continue
}
fmt.Printf("товар: %s\n", p.Name)
}
}
}
}
Потребление памяти пропорционально размеру одного <product>, а не всего файла.
Генерация XML
xml.MarshalIndent создаёт читаемый XML с отступами. Важный нюанс: стандартная библиотека не пишет XML-декларацию <?xml ...?> автоматически — её нужно добавить руками.
output, err := xml.MarshalIndent(cat, "", " ")
if err != nil {
fmt.Println("ошибка маршалинга:", err)
return
}
// Добавляем декларацию
result := []byte(xml.Header + string(output))
os.WriteFile("out.xml", result, 0644)
xml.Header — это константа <?xml version="1.0" encoding="UTF-8"?>\n. Если пишете в файл без неё, некоторые парсеры на принимающей стороне (особенно в Java/.NET) откажутся читать документ.
Пространства имён и кодировки
Два самых частых источника боли:
- Namespace. Если XML содержит
xmlns, Go учитывает его при маппинге. Поле должно содержать полный URI:xml:"http://example.com/ns name". Пропустите — поле останется пустым, без ошибки. - Кодировка Windows-1251. Стандартная библиотека поддерживает только UTF-8. Для windows-1251 (частый случай с 1С-выгрузками) нужно подключить
golang.org/x/text/encoding/charmapи обернутьio.Readerчерезtransform.NewReaderдо передачи вxml.NewDecoder.
Если XML приходит в windows-1251 и вы передаёте его в xml.Unmarshal без перекодировки, парсер не упадёт — он тихо превратит кириллицу в мусор. Ошибку вы увидите только тогда, когда данные уже в базе.
CSV: encoding/csv
Чтение
Пакет encoding/csv работает по RFC 4180, но реальные файлы часто не соответствуют стандарту. Минимальный пример:
package main
import (
"encoding/csv"
"fmt"
"strings"
)
func main() {
input := "name;price;qty\nКабель USB-C;490.00;120\nАдаптер HDMI;1120.50;45"
reader := csv.NewReader(strings.NewReader(input))
reader.Comma = ';' // разделитель — точка с запятой
records, err := reader.ReadAll()
if err != nil {
fmt.Println("ошибка чтения:", err)
return
}
for i, row := range records {
if i == 0 {
continue // пропускаем заголовок
}
fmt.Printf("%s — %s руб., остаток %s\n", row[0], row[1], row[2])
}
}
Проверка: две строки с товарами. Если видите ошибку wrong number of fields — в одной из строк другое количество разделителей. Об этом ниже.
Потоковое чтение
Для больших файлов вместо ReadAll используйте Read в цикле — аналогично потоковому XML:
for {
record, err := reader.Read()
if err != nil {
break // io.EOF или ошибка
}
// обработка record
}
Запись
package main
import (
"encoding/csv"
"os"
)
func main() {
file, _ := os.Create("export.csv")
defer file.Close()
// BOM для Excel: без него кириллица в UTF-8 отобразится кракозябрами
file.Write([]byte{0xEF, 0xBB, 0xBF})
writer := csv.NewWriter(file)
writer.Comma = ';'
writer.Write([]string{"Название", "Цена", "Остаток"})
writer.Write([]string{"Кабель USB-C", "490.00", "120"})
writer.Write([]string{"Адаптер HDMI", "1120.50", "45"})
writer.Flush()
if err := writer.Error(); err != nil {
panic(err)
}
}
Обратите внимание на writer.Flush(). Без него последние записи останутся в буфере и не попадут в файл. Это классическая ошибка, которую легко пропустить на маленьких файлах: буфер сбрасывается при закрытии файла, и всё работает — до тех пор, пока файл не станет большим и defer file.Close() не отработает раньше, чем вы ожидаете.
Сравнение подходов: XML vs CSV
| Критерий | encoding/xml | encoding/csv |
|---|---|---|
| Вложенная структура | Поддерживается нативно | Только плоские строки |
| Типизация при чтении | Автоматическая (int, float64, bool через теги) | Всё приходит строками — конвертируйте сами |
| Потоковое чтение | xml.Decoder | csv.Reader.Read |
| Кодировки кроме UTF-8 | Нужен golang.org/x/text | Нужен golang.org/x/text |
| Работа с Excel | Неактуально | Нужен BOM + точка с запятой |
| Скорость на 100 000 записей* | Медленнее в 3–5 раз | Быстрее, формат проще |
* Порядок зависит от структуры данных и глубины вложенности; замеряйте на своих файлах с помощью go test -bench.
Типичные ошибки и как их избежать
- Молчаливо пропущенные поля в XML. Опечатка в теге
xml:"proce"вместоxml:"price"— и поле остаётся нулевым без ошибки. Совет: послеUnmarshalсравнивайте количество заполненных записей с ожидаемым. - Разное количество колонок в CSV. По умолчанию
csv.Readerсчитает количество полей в первой строке эталоном. Если строка имеет другое число полей — ошибка. Установитеreader.FieldsPerRecord = -1, если количество полей плавает. - Забытый
Flushпри записи CSV. Данные остаются в буфере. Всегда вызывайтеwriter.Flush()и проверяйтеwriter.Error(). - Кракозябры в Excel. Excel на Windows по умолчанию ожидает CSV в кодировке с BOM или в windows-1251. UTF-8 без BOM — гарантированные кракозябры. Три байта
0xEF, 0xBB, 0xBFв начале файла решают проблему. - Отсутствие XML-декларации. Некоторые системы-получатели (SAP, 1С) требуют
<?xml version="1.0" encoding="UTF-8"?>.xml.Marshalеё не добавляет — пишитеxml.Headerвручную.
Сноска: BOM (Byte Order Mark) — невидимая последовательность байт в начале файла, которая сообщает программе-читателю кодировку документа. Для UTF-8 это три байта: EF BB BF.
Что делать дальше
После того как парсинг и генерация работают, следующий шаг — валидация. Для XML это может быть проверка по XSD-схеме (стандартная библиотека Go её не поддерживает; посмотрите на внешние пакеты или валидируйте на стороне получателя). Для CSV — проверка типов и диапазонов значений после чтения, потому что encoding/csv ничего не знает о семантике ваших данных.
Если объёмы растут до миллионов строк, замерьте потоковый парсинг через go test -bench -benchmem — аллокации на каждой итерации могут стать узким местом. Для XML на экстремальных объёмах стоит посмотреть в сторону SAX-подобного подхода, который вы уже видели в примере с Decoder.Token(), и не декодировать элементы, которые вам не нужны.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: encoding/xml — стандартная библиотека Go, encoding/csv — стандартная библиотека Go
Частые вопросы
Почему после xml.Unmarshal часть полей пустая, хотя в файле данные есть?
Как парсить XML в кодировке windows-1251?
Почему Excel показывает кракозябры при открытии моего CSV?
Файл CSV на 500 МБ — ReadAll зависает. Что делать?
Можно ли автоматически маппить CSV-строки на Go-структуры, как xml.Unmarshal?
У меня Go 1.20, примеры будут работать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.