Кодирование: 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для boolnilдля указателей, 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"-- сырой XMLxml:",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 (не интероперабельный)