Easy📖Теория8 min

Кодирование: JSON, XML, CSV

encoding/json: маршалинг, struct tags, streaming, кастомные кодеки и другие форматы

Кодирование: JSON, XML, CSV

JSON -- основной формат обмена данными в веб-разработке. Go предоставляет пакет encoding/json с мощными возможностями: struct tags, streaming, кастомные кодеки и строгий парсинг. Также рассмотрим encoding/xml, encoding/csv и encoding/gob.

json.Marshal и json.Unmarshal

Marshal: Go -> JSON

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

user := User{
    ID:        1,
    Name:      "Alice",
    Email:     "[email protected]",
    CreatedAt: time.Now(),
}

// Marshal to JSON bytes
data, err := json.Marshal(user)
if err != nil {
    log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {"id":1,"name":"Alice","email":"[email protected]","created_at":"2025-01-15T10:30:00Z"}

// Pretty print
data, err = json.MarshalIndent(user, "", "  ")
// {
//   "id": 1,
//   "name": "Alice",
//   "email": "[email protected]",
//   "created_at": "2025-01-15T10:30:00Z"
// }

Unmarshal: JSON -> Go

jsonStr := `{"id": 1, "name": "Alice", "email": "[email protected]"}`

var user User
if err := json.Unmarshal([]byte(jsonStr), &user); err != nil {
    log.Fatalf("unmarshal: %v", err)
}
fmt.Printf("%+v\n", user)
// {ID:1 Name:Alice Email:[email protected] CreatedAt:0001-01-01 00:00:00 +0000 UTC}

Struct Tags

Основные теги

type Product struct {
    ID          int     `json:"id"`                  // Rename field
    Name        string  `json:"name"`                // Lowercase
    Description string  `json:"description"`         // Lowercase
    Price       float64 `json:"price"`               // Lowercase
    InternalID  string  `json:"-"`                   // SKIP -- never in JSON
    Category    string  `json:"category,omitempty"`   // Omit if empty string
    Stock       int     `json:"stock,omitempty"`      // Omit if zero
    Tags        []string `json:"tags,omitempty"`      // Omit if nil/empty slice
    Metadata    map[string]string `json:"metadata,omitempty"` // Omit if nil/empty map
}

omitempty vs omitzero (Go 1.24+)

omitempty пропускает "пустые" значения, но его определение "пустого" не всегда интуитивно:

  • 0 для чисел
  • "" для строк
  • false для bool
  • nil для указателей, slices, maps
  • Не работает с time.Time (не пустая структура)

Go 1.24 добавил omitzero -- более предсказуемый вариант:

type Event struct {
    Name string    `json:"name"`

    // omitempty: time.Time NEVER omitted (it's a non-empty struct)
    StartAt time.Time `json:"start_at,omitempty"` // Always included

    // omitzero: time.Time omitted when IsZero() returns true
    EndAt time.Time `json:"end_at,omitzero"` // Omitted if zero time

    // Works with custom types that implement IsZero()
    Status Status `json:"status,omitzero"`
}

type Status struct {
    Code int
}

// IsZero makes Status work with omitzero
func (s Status) IsZero() bool {
    return s.Code == 0
}
event := Event{
    Name:    "Conference",
    StartAt: time.Time{}, // Zero time
    EndAt:   time.Time{}, // Zero time
}

data, _ := json.Marshal(event)
fmt.Println(string(data))
// With omitempty on StartAt: {"name":"Conference","start_at":"0001-01-01T00:00:00Z","end_at":...}
// With omitzero on EndAt:    {"name":"Conference","start_at":"0001-01-01T00:00:00Z"}

Рекомендация: Используйте omitzero для time.Time и любых типов с методом IsZero(). Для примитивов omitempty работает нормально.

json.Encoder и json.Decoder (Streaming)

Для работы с потоками (файлы, HTTP, сеть) используйте Encoder/Decoder вместо Marshal/Unmarshal:

// Writing JSON to HTTP response
func listUsers(w http.ResponseWriter, r *http.Request) {
    users := getUsers()

    w.Header().Set("Content-Type", "application/json")
    // Encoder writes directly to ResponseWriter -- no intermediate []byte
    if err := json.NewEncoder(w).Encode(users); err != nil {
        log.Printf("encoding response: %v", err)
    }
}

// Reading JSON from HTTP request
func createUser(w http.ResponseWriter, r *http.Request) {
    var user User

    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields() // Reject unknown fields

    if err := dec.Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    // ...
}

Обработка NDJSON (newline-delimited JSON)

// Read NDJSON stream
func processStream(r io.Reader) error {
    dec := json.NewDecoder(r)

    for dec.More() {
        var event Event
        if err := dec.Decode(&event); err != nil {
            return fmt.Errorf("decoding event: %w", err)
        }
        processEvent(event)
    }

    return nil
}

// Write NDJSON stream
func writeStream(w io.Writer, events []Event) error {
    enc := json.NewEncoder(w)
    for _, event := range events {
        if err := enc.Encode(event); err != nil {
            return fmt.Errorf("encoding event: %w", err)
        }
    }
    return nil
}

json.RawMessage -- частичный парсинг

json.RawMessage откладывает парсинг части JSON:

// Message with dynamic payload
type Envelope struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"` // Parsed later
}

func handleMessage(data []byte) error {
    var env Envelope
    if err := json.Unmarshal(data, &env); err != nil {
        return fmt.Errorf("unmarshaling envelope: %w", err)
    }

    switch env.Type {
    case "user_created":
        var user User
        if err := json.Unmarshal(env.Payload, &user); err != nil {
            return fmt.Errorf("unmarshaling user: %w", err)
        }
        return handleUserCreated(user)

    case "order_placed":
        var order Order
        if err := json.Unmarshal(env.Payload, &order); err != nil {
            return fmt.Errorf("unmarshaling order: %w", err)
        }
        return handleOrderPlaced(order)

    default:
        return fmt.Errorf("unknown message type: %s", env.Type)
    }
}

Кастомный MarshalJSON / UnmarshalJSON

Реализуйте интерфейсы json.Marshaler и json.Unmarshaler для кастомной сериализации:

type Duration struct {
    time.Duration
}

// MarshalJSON encodes Duration as a human-readable string.
func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String()) // "5m30s"
}

// UnmarshalJSON decodes Duration from a string.
func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }

    dur, err := time.ParseDuration(s)
    if err != nil {
        return fmt.Errorf("invalid duration %q: %w", s, err)
    }

    d.Duration = dur
    return nil
}

// Usage
type Config struct {
    Timeout  Duration `json:"timeout"`
    Interval Duration `json:"interval"`
}

// JSON: {"timeout": "30s", "interval": "5m"}

Enum с кастомным JSON

type Role int

const (
    RoleViewer Role = iota
    RoleEditor
    RoleAdmin
)

var roleNames = map[Role]string{
    RoleViewer: "viewer",
    RoleEditor: "editor",
    RoleAdmin:  "admin",
}

var roleValues = map[string]Role{
    "viewer": RoleViewer,
    "editor": RoleEditor,
    "admin":  RoleAdmin,
}

func (r Role) MarshalJSON() ([]byte, error) {
    name, ok := roleNames[r]
    if !ok {
        return nil, fmt.Errorf("unknown role: %d", r)
    }
    return json.Marshal(name)
}

func (r *Role) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }

    val, ok := roleValues[s]
    if !ok {
        return fmt.Errorf("unknown role: %q", s)
    }

    *r = val
    return nil
}

Обработка неизвестных полей

// Strict decoding: reject unknown fields
func strictDecode(data []byte, v any) error {
    dec := json.NewDecoder(bytes.NewReader(data))
    dec.DisallowUnknownFields()

    if err := dec.Decode(v); err != nil {
        return fmt.Errorf("decoding: %w", err)
    }
    return nil
}

// Capture unknown fields
type FlexibleUser struct {
    Name  string `json:"name"`
    Email string `json:"email"`

    // Extra fields go here
    Extra map[string]json.RawMessage `json:"-"`
}

func (u *FlexibleUser) UnmarshalJSON(data []byte) error {
    // First, decode known fields
    type Alias FlexibleUser // Avoid infinite recursion
    var alias Alias
    if err := json.Unmarshal(data, &alias); err != nil {
        return err
    }
    *u = FlexibleUser(alias)

    // Then, capture all fields into a map
    var all map[string]json.RawMessage
    if err := json.Unmarshal(data, &all); err != nil {
        return err
    }

    // Remove known fields
    delete(all, "name")
    delete(all, "email")

    u.Extra = all
    return nil
}

json.Number -- произвольная точность

По умолчанию JSON числа декодируются как float64, что может потерять точность:

// Problem: large integers lose precision in float64
data := `{"id": 9007199254740993}` // > 2^53
var m map[string]any
json.Unmarshal([]byte(data), &m)
fmt.Println(m["id"]) // 9.007199254740992e+15 -- WRONG!

// Solution: use json.Number
dec := json.NewDecoder(strings.NewReader(data))
dec.UseNumber()

var m2 map[string]any
dec.Decode(&m2)

num := m2["id"].(json.Number)
fmt.Println(num.String()) // "9007199254740993" -- correct

// Convert to int64
n, err := num.Int64()
fmt.Println(n) // 9007199254740993

JSON и time.Time

По умолчанию time.Time сериализуется в RFC 3339:

type Event struct {
    Name string    `json:"name"`
    Date time.Time `json:"date"` // "2025-01-15T10:30:00Z"
}

// Custom time format
type CustomTime struct {
    time.Time
}

const customLayout = "2006-01-02"

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(ct.Format(customLayout))
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }

    t, err := time.Parse(customLayout, s)
    if err != nil {
        return fmt.Errorf("parsing time %q: %w", s, err)
    }

    ct.Time = t
    return nil
}

type Birthday struct {
    Name string     `json:"name"`
    Date CustomTime `json:"date"` // "1990-05-15"
}

encoding/xml -- основы

type Feed struct {
    XMLName xml.Name `xml:"feed"`
    Title   string   `xml:"title"`
    Entries []Entry  `xml:"entry"`
}

type Entry struct {
    Title   string `xml:"title"`
    Link    Link   `xml:"link"`
    Content string `xml:"content"`
}

type Link struct {
    Href string `xml:"href,attr"` // XML attribute
    Rel  string `xml:"rel,attr"`
}

// Marshal
feed := Feed{
    Title: "My Blog",
    Entries: []Entry{
        {Title: "Post 1", Content: "Hello"},
    },
}
data, _ := xml.MarshalIndent(feed, "", "  ")

// Unmarshal
var parsed Feed
xml.Unmarshal(xmlData, &parsed)

XML-специфичные теги:

  • xml:"name" -- имя элемента
  • xml:"name,attr" -- XML-атрибут
  • xml:",chardata" -- текстовое содержимое элемента
  • xml:",cdata" -- CDATA секция
  • xml:",innerxml" -- сырой XML
  • xml:",comment" -- XML-комментарий
  • xml:"a>b" -- вложенный элемент <a><b>...</b></a>

encoding/csv -- основы

import "encoding/csv"

// Write CSV
func writeCSV(w io.Writer, records [][]string) error {
    writer := csv.NewWriter(w)
    defer writer.Flush()

    // Write header
    if err := writer.Write([]string{"Name", "Email", "Age"}); err != nil {
        return err
    }

    // Write records
    for _, record := range records {
        if err := writer.Write(record); err != nil {
            return err
        }
    }

    return writer.Error()
}

// Read CSV
func readCSV(r io.Reader) ([][]string, error) {
    reader := csv.NewReader(r)
    reader.FieldsPerRecord = -1 // Allow variable field count
    reader.TrimLeadingSpace = true

    return reader.ReadAll()
}

// Read CSV line by line (memory-efficient for large files)
func processCSV(r io.Reader) error {
    reader := csv.NewReader(r)

    // Read header
    header, err := reader.Read()
    if err != nil {
        return fmt.Errorf("reading header: %w", err)
    }
    fmt.Println("Columns:", header)

    // Read rows
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("reading row: %w", err)
        }
        processRow(record)
    }

    return nil
}

encoding/gob -- Go-to-Go сериализация

gob -- бинарный формат, оптимизированный для Go-типов. Быстрее и компактнее JSON, но работает только между Go-программами:

import "encoding/gob"

// Encode
func encodeGob(user User) ([]byte, error) {
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)

    if err := enc.Encode(user); err != nil {
        return nil, fmt.Errorf("encoding: %w", err)
    }

    return buf.Bytes(), nil
}

// Decode
func decodeGob(data []byte) (User, error) {
    var user User
    dec := gob.NewDecoder(bytes.NewReader(data))

    if err := dec.Decode(&user); err != nil {
        return User{}, fmt.Errorf("decoding: %w", err)
    }

    return user, nil
}

// For interfaces, register concrete types
func init() {
    gob.Register(User{})
    gob.Register(Admin{})
}

Когда использовать gob:

  • Кэширование Go-объектов
  • IPC между Go-процессами
  • Сохранение состояния на диск
  • Не для API (не интероперабельный)

Проверь себя

🧪

В чём разница между omitempty и omitzero (Go 1.24+) для time.Time?

🧪

Для чего используется json.RawMessage?

🧪

Почему числа из JSON могут потерять точность при декодировании в map[string]any?

🧪

Когда использовать json.Encoder/Decoder вместо Marshal/Unmarshal?