1. Code
  2. Coding Fundamentals

Сериализация JSON с Golang

Scroll to top

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Обзор

JSON - один из самых популярных форматов сериализации. Он удобен для чтения, достаточно лаконичен и может быть легко проанализирован любым веб-приложением, использующим JavaScript. Go как современный язык программирования имеет первоклассную поддержку сериализации JSON в своей стандартной библиотеке.

Но есть и укромные уголки. В этом руководстве вы узнаете, как эффективно сериализовать и десериализовать произвольные, а также структурированные данные в/из JSON. Вы также узнаете, как работать с расширенными сценариями, такими как перечисления сериализации.

Пакет JSON

Go поддерживает несколько форматов сериализации в пакете кодирования своей стандартной библиотеки. Одним из них является популярный формат JSON. Вы сериализуете значения Golang, используя функцию Marshal(), в кусочек байтов. Вы десериализуете часть байтов в значение Golang, используя функцию Unmarshal(). Это так просто. Следующие термины эквивалентны в контексте этой статьи:

  • Сериализация/Кодирование/Маршалинг
  • Десериализация/Декодирование/Демаршаллизация

Я предпочитаю сериализацию, потому что она отражает тот факт, что вы преобразуете потенциально иерархическую структуру данных в/из потока байтов.

Marshal

Функция Marshal() может принимать что угодно, что в Go означает пустой интерфейс и возвращает часть байтов и ошибку. Вот ее сигнатура:

func Marshal(v interface{}) ([]byte, error)

Если Marshal() не сможет сериализовать входное значение, она вернет ненулевую ошибку. У Marshal() есть некоторые строгие ограничения (позже мы увидим, как их преодолеть с помощью пользовательских маршаллеров):

  • Ключи карты должны быть строками.
  • Значения карты должны иметь типы, сериализуемые пакетом json.
  • Следующие типы не поддерживаются: Channel, complex и function.
  • Циклические структуры данных не поддерживаются.
  • Указатели будут закодированы (и позже декодированы) как значения, на которые они указывают (или «null», если указатель равен нулю).

Unmarshal

Функция Unmarshal() принимает фрагмент байта, который, как мы надеемся, представляет действительный JSON, и целевой интерфейс, который обычно является указателем на структуру или базовый тип. Он десериализует JSON в интерфейс универсальным способом. Если сериализация не удалась, он вернет ошибку. Вот сигнатура:

func Unmarshal(data []byte, v interface{}) error

Сериализация простых типов

Вы можете легко сериализовать простые типы, например, используя пакет json. Результатом будет не полноценный объект JSON, а простая строка. Здесь int 5 сериализуется в байтовый массив [53], который соответствует строке «5».

1
    // Serialize int

2
    var x = 5
3
    bytes, err := json.Marshal(x)
4
    if err != nil {
5
        fmt.Println("Can't serislize", x)
6
    }
7
8
    fmt.Printf("%v => %v, '%v'\n", x, bytes, string(bytes))
9
10
    // Deserialize int

11
    var r int
12
    err = json.Unmarshal(bytes, &r)
13
    if err != nil {
14
        fmt.Println("Can't deserislize", bytes)
15
    }
16
17
    fmt.Printf("%v => %v\n", bytes, r)
18
    
19
Output:
20
21
- 5 => [53], '5'
22
- [53] => 5

Если вы попытаетесь сериализовать неподдерживаемые типы, такие как функция, вы получите ошибку:

1
    // Trying to serialize a function

2
    foo := func() {
3
        fmt.Println("foo() here")
4
    }
5
6
    bytes, err = json.Marshal(foo)
7
    if err != nil {
8
        fmt.Println(err)
9
    }
10
11
Output:
12
13
json: unsupported type: func()

Сериализация произвольных данных с помощью карт

Сила JSON в том, что он может очень хорошо представлять произвольные иерархические данные. Пакет JSON поддерживает его и использует общий пустой интерфейс (interface {}) для представления любой иерархии JSON. Вот пример десериализации и последующей сериализации двоичного дерева, где каждый узел имеет значение int и две ветви, левую и правую, которые могут содержать другой узел или быть нулевыми.

JSON null эквивалентен Go nil. Как видно из выходных данных, функция json.Unmarshal() успешно преобразовала большой двоичный объект JSON в структуру данных Go, состоящую из вложенной карты интерфейсов, и сохранила тип значения как int. Функция json.Marshal() успешно сериализовала полученный вложенный объект в то же представление JSON.

1
    // Arbitrary nested JSON

2
    dd := `
3
    {
4
        "value": 3,
5
        "left": {
6
            "value": 1,
7
            "left": null,
8
            "right": {
9
                "value": 2,
10
                "left": null,
11
                "right": null
12
            }
13
        },
14
        "right": {
15
            "value": 4,
16
            "left": null,
17
            "right": null
18
         }
19
    }`
20
21
    var obj interface{}
22
    err = json.Unmarshal([]byte(dd), &obj)
23
    if err != nil {
24
        fmt.Println(err)
25
    } else {
26
        fmt.Println("--------\n", obj)
27
    }
28
29
30
    data, err = json.Marshal(obj)
31
    if err != nil {
32
        fmt.Println(err)
33
    } else {
34
        fmt.Println("--------\n", string(data))
35
    }
36
}
37
38
Output:
39
40
--------
41
 map[right:map[value:4 
42
               left:<nil> 
43
               right:<nil>] 
44
     value:3 
45
     left:map[left:<nil> 
46
              right:map[value:2 
47
                        left:<nil> 
48
                        right:<nil>] 
49
              value:1]]
50
--------
51
 {"left":{
52
      "left":null,
53
      "right":{"left":null,"right":null,"value":2},
54
      "value":1},
55
  "right":{"left":null,
56
           "right":null,
57
           "value":4},
58
  "value":3}

Чтобы пройти общие карты интерфейсов, вам нужно использовать утверждения типа. Например:

1
func dump(obj interface{}) error {
2
    if obj == nil {
3
        fmt.Println("nil")
4
        return nil
5
    }
6
    switch obj.(type) {
7
    case bool:
8
        fmt.Println(obj.(bool))
9
    case int:
10
        fmt.Println(obj.(int))
11
    case float64:
12
        fmt.Println(obj.(float64))
13
    case string:
14
        fmt.Println(obj.(string))
15
    case map[string]interface{}:
16
        for k, v := range(obj.(map[string]interface{})) {
17
            fmt.Printf("%s: ", k)
18
            err := dump(v)
19
            if err != nil {
20
                return err
21
            }
22
        }
23
    default:
24
        return errors.New(
25
            fmt.Sprintf("Unsupported type: %v", obj))
26
    }
27
28
    return nil
29
}

Сериализация структурированных данных

Работа со структурированными данными часто является лучшим выбором. Go предоставляет отличную поддержку для сериализации JSON в/из структур через его теги struct. Давайте создадим struct, которая соответствует нашему дереву JSON и более умной функции Dump(), которая ее печатает:

1
type Tree struct {
2
    value int
3
    left *Tree
4
    right *Tree
5
}
6
7
func (t *Tree) Dump(indent string) {
8
    fmt.Println(indent + "value:", t.value)
9
    fmt.Print(indent + "left: ")
10
    if t.left == nil {
11
        fmt.Println(nil)
12
    } else {
13
        fmt.Println()
14
        t.left.Dump(indent + "  ")
15
    }
16
17
    fmt.Print(indent + "right: ")
18
    if t.right == nil {
19
        fmt.Println(nil)
20
    } else {
21
        fmt.Println()
22
        t.right.Dump(indent + "  ")
23
    }
24
}

Это здорово и намного чище, чем произвольный подход JSON. Но работает ли это? На самом деле, нет. Там нет ошибки, но наш объект дерева не заполняется JSON.

1
    jsonTree := `
2
    {
3
        "value": 3,
4
        "left": {
5
            "value": 1,
6
            "left": null,
7
            "right": {
8
                "value": 2,
9
                "left": null,
10
                "right": null
11
            }
12
        },
13
        "right": {
14
            "value": 4,
15
            "left": null,
16
            "right": null
17
         }
18
    }`
19
20
21
    var tree Tree
22
    err = json.Unmarshal([]byte(dd), &tree)
23
    if err != nil {
24
        fmt.Printf("- Can't deserislize tree, error: %v\n", err)
25
    } else {
26
        tree.Dump("")
27
    }
28
29
Output:
30
31
value: 0
32
left: <nil>
33
right: <nil>

Проблема в том, что поля дерева являются приватными. Сериализация JSON работает только для открытых полей. Таким образом, мы можем сделать поля структуры общедоступными. Пакет json достаточно умен, чтобы прозрачно преобразовать строчные буквы «value», «left» и «right» в соответствующие им имена полей верхнего регистра.

1
type Tree struct {
2
    Value int   `json:"value"`
3
    Left  *Tree `json:"left"`
4
    Right *Tree `json:"right"`
5
}
6
7
8
Output:
9
10
value: 3
11
left: 
12
  value: 1
13
  left: <nil>
14
  right: 
15
    value: 2
16
    left: <nil>
17
    right: <nil>
18
right: 
19
  value: 4
20
  left: <nil>
21
  right: <nil>

Пакет json будет автоматически игнорировать не отображенные поля в JSON, а также приватные поля в вашей struct. Но иногда вам может потребоваться отобразить определенные ключи в JSON на поле с другим именем в вашей struct. Вы можете использовать теги struct для этого. Например, предположим, что мы добавили еще одно поле с именем «label» в JSON, но нам нужно сопоставить его с полем «Tag» в нашей структуре.

1
type Tree struct {
2
    Value int
3
    Tag string    `json:"label"`
4
    Left  *Tree
5
    Right *Tree
6
}
7
8
func (t *Tree) Dump(indent string) {
9
    fmt.Println(indent + "value:", t.Value)
10
    if t.Tag != "" {
11
        fmt.Println(indent + "tag:", t.Tag)
12
    }
13
    fmt.Print(indent + "left: ")
14
    if t.Left == nil {
15
        fmt.Println(nil)
16
    } else {
17
        fmt.Println()
18
        t.Left.Dump(indent + "  ")
19
    }
20
21
    fmt.Print(indent + "right: ")
22
    if t.Right == nil {
23
        fmt.Println(nil)
24
    } else {
25
        fmt.Println()
26
        t.Right.Dump(indent + "  ")
27
    }
28
}

Вот новый JSON с корневым узлом дерева, помеченным как «root», правильно сериализованный в поле Tag и напечатанный в выходных данных:

1
    dd := `
2
    {
3
        "label": "root",
4
        "value": 3,
5
        "left": {
6
            "value": 1,
7
            "left": null,
8
            "right": {
9
                "value": 2,
10
                "left": null,
11
                "right": null
12
            }
13
        },
14
        "right": {
15
            "value": 4,
16
            "left": null,
17
            "right": null
18
         }
19
    }`
20
21
22
    var tree Tree
23
    err = json.Unmarshal([]byte(dd), &tree)
24
    if err != nil {
25
        fmt.Printf("- Can't deserislize tree, error: %v\n", err)
26
    } else {
27
        tree.Dump("")
28
    }
29
30
Output:
31
32
value: 3
33
tag: root
34
left: 
35
  value: 1
36
  left: <nil>
37
  right: 
38
    value: 2
39
    left: <nil>
40
    right: <nil>
41
right: 
42
  value: 4
43
  left: <nil>
44
  right: <nil>

Написание обычного упаковщика

Вы часто захотите сериализовать объекты, которые не соответствуют строгим требованиям функции Marshal(). Например, вы можете захотеть сериализовать карту с помощью ключей int. В этих случаях вы можете написать собственный упаковщик/распаковщик, реализовав интерфейсы Marshaler и Unmarshaler.

Примечание о правописании. В Go принято называть интерфейс одним методом, добавляя суффикс «er» к имени метода. Таким образом, несмотря на то, что более распространенным написанием является «Marshaller» (с двойным L), имя интерфейса - просто «Marshaler» (один L).

Вот интерфейсы Marshaler и Unmarshaler:

1
type Marshaler interface {
2
        MarshalJSON() ([]byte, error)
3
}
4
5
type Unmarshaler interface {
6
        UnmarshalJSON([]byte) error
7
}

Вы должны создать тип при выполнении пользовательской сериализации, даже если вы хотите сериализовать встроенный тип или композицию встроенных типов, таких как map[int]string. Здесь я определяю тип с именем IntStringMap и реализую интерфейсы Marshaler и Unmarshaler для этого типа.

Метод MarshalJSON() создает map[string]string, преобразует каждый из своих собственных ключей int в строку и сериализует карту со строковыми ключами, используя стандартную функцию json.Marshal().

1
type IntStringMap map[int]string
2
3
func (m *IntStringMap) MarshalJSON() ([]byte, error) {
4
    ss := map[string]string{}
5
    for k, v := range *m {
6
        i := strconv.Itoa(k)
7
        ss[i] = v
8
    }
9
    return json.Marshal(ss)
10
}

Метод UnmarshalJSON() делает прямо противоположное. Он десериализует массив байтов данных в map[string]string, а затем преобразует каждый строковый ключ в int и заполняет сам себя.

1
func (m *IntStringMap) UnmarshalJSON(data []byte ) error {
2
    ss := map[string]string{}
3
    err := json.Unmarshal(data, &ss)
4
    if err != nil {
5
        return err
6
    }
7
    for k, v := range ss {
8
        i, err := strconv.Atoi(k)
9
        if err != nil {
10
            return err
11
        }
12
        (*m)[i] = v
13
    }
14
    return nil
15
}

Вот как это использовать в программе:

1
    m := IntStringMap{4: "four", 5: "five"}
2
    data, err := m.MarshalJSON()
3
    if err != nil {
4
        fmt.Println(err)
5
    }
6
    fmt.Println("IntStringMap to JSON: ", string(data))
7
8
9
    m = IntStringMap{}
10
11
    jsonString := []byte("{\"1\": \"one\", \"2\": \"two\"}")
12
    m.UnmarshalJSON(jsonString)
13
14
    fmt.Printf("IntStringMap from JSON: %v\n", m)
15
    fmt.Println("m[1]:", m[1], "m[2]:", m[2])
16
17
Output:
18
19
IntStringMap to JSON:  {"4":"four","5":"five"}
20
IntStringMap from JSON: map[2:two 1:one]
21
m[1]: one m[2]: two

Сериализация Enums

Перечисления Go могут быть довольно неприятными для сериализации. Идея написать статью о сериализации Go json возникла из-за вопроса, который мне спросил коллега о том, как сериализовать перечисления. Вот enum Go. Константы Zero и One равны целым числам 0 и 1.

1
type EnumType int
2
3
const (
4
    Zero     EnumType = iota
5
    One
6
)

Хотя вы можете думать, что это int, и во многих отношениях это так, вы не можете сериализовать его напрямую. Вы должны написать собственный маршалер/демаршалер. Это не проблема после последнего раздела. Следующие MarshalJSON() и UnmarshalJSON() будут сериализовать/десериализовать константы ZERO и ONE в/из соответствующих строк «Zero» и «One».

1
func (e *EnumType) UnmarshalJSON(data []byte) error {
2
    var s string
3
    err := json.Unmarshal(data, &s)
4
    if err != nil {
5
        return err
6
    }
7
8
    value, ok := map[string]EnumType{"Zero": Zero, "One": One}[s]
9
    if !ok {
10
        return errors.New("Invalid EnumType value")
11
    }
12
    *e = value
13
    return nil
14
}
15
16
func (e *EnumType) MarshalJSON() ([]byte, error) {
17
    value, ok := map[EnumType]string{Zero: "Zero", One:"One"}[*e]
18
    if !ok {
19
        return nil, errors.New("Invalid EnumType value")
20
    }
21
    return json.Marshal(value)
22
}

Давайте попробуем встроить этот EnumType в struct и сериализовать его. Основная функция создает EnumContainer и инициализирует его с именем «Uno» и значением нашей константы enum ONE, которая равна int 1.

1
type EnumContainer struct {
2
    Name                string
3
    Value               EnumType
4
}
5
6
7
func main() {
8
    x := One
9
    ec := EnumContainer{
10
        "Uno",
11
        x,
12
    }
13
    s, err := json.Marshal(ec)
14
    if err != nil {
15
        fmt.Printf("fail!")
16
    }
17
18
    var ec2 EnumContainer
19
    err = json.Unmarshal(s, &ec2)
20
21
    fmt.Println(ec2.Name, ":", ec2.Value)
22
}
23
24
Output:
25
26
Uno : 0

Ожидаемый результат - «Uno: 1», но вместо этого «Uno: 0». Что случилось? В коде упаковщика/распаковщика нет ошибки. Оказывается, вы не можете встраивать перечисления по значению, если хотите их сериализовать. Вы должны вставить указатель на перечисление. Вот модифицированная версия, где она работает как положено:

1
type EnumContainer struct {
2
    Name                string
3
    Value               *EnumType
4
}
5
6
func main() {
7
    x := One
8
    ec := EnumContainer{
9
        "Uno",
10
        &x,
11
    }
12
    s, err := json.Marshal(ec)
13
    if err != nil {
14
        fmt.Printf("fail!")
15
    }
16
17
    var ec2 EnumContainer
18
    err = json.Unmarshal(s, &ec2)
19
20
    fmt.Println(ec2.Name, ":", *ec2.Value)
21
}
22
23
Output:
24
25
Uno : 1

Заключение

Go предоставляет много опций для сериализации и десериализации JSON. Важно понять все входы и выходы пакета encoding/json, чтобы воспользоваться преимуществами.

В этом уроке вы получите всю мощь, в том числе и сериализацию неуловимых перечислений Go.

Иди и сериализуй некоторые объекты!