обзор
JSON — один из самых популярных форматов сериализации. Он удобен для чтения, достаточно лаконичен и может быть легко проанализирован любым веб-приложением, использующим JavaScript. Go как современный язык программирования имеет первоклассную поддержку сериализации JSON в своей стандартной библиотеке.
Но есть и укромные уголки. В этом руководстве вы узнаете, как эффективно сериализовать и десериализовать произвольные, а также структурированные данные в / из JSON. Вы также узнаете, как работать с расширенными сценариями, такими как перечисления сериализации.
Пакет JSON
Go поддерживает несколько форматов сериализации в пакете кодирования своей стандартной библиотеки. Одним из них является популярный формат JSON. Вы сериализуете значения Голанга, используя функцию Marshal (), в кусочек байтов. Вы десериализуете часть байтов в значение Golang, используя функцию Unmarshal (). Это так просто. Следующие термины эквивалентны в контексте этой статьи:
- Сериализация / Кодирование / Ранжирование
- Десериализация / Декодирование / Демаршаллизация
Я предпочитаю сериализацию, потому что она отражает тот факт, что вы преобразуете потенциально иерархическую структуру данных в / из потока байтов.
маршал
Функция Marshal () может принимать все, что в Go означает пустой интерфейс и возвращать часть байтов и ошибку. Вот подпись:
func Marshal(v interface{}) ([]byte, error)
Если Marshal () не сможет сериализовать входное значение, он вернет ненулевую ошибку. У Marshal () есть некоторые строгие ограничения (позже мы увидим, как их преодолеть с помощью пользовательских маршаллеров):
- Ключи карты должны быть строками.
- Значения карты должны иметь типы, сериализуемые пакетом json.
- Следующие типы не поддерживаются: Channel, complex и function.
- Циклические структуры данных не поддерживаются.
- Указатели будут закодированы (и позже декодированы) как значения, на которые они указывают (или «ноль», если указатель равен нулю).
распаковать
Функция Unmarshal () принимает фрагмент байта, который, как мы надеемся, представляет действительный JSON, и целевой интерфейс, который обычно является указателем на структуру или базовый тип. Он десериализует JSON в интерфейс универсальным способом. Если сериализация не удалась, он вернет ошибку. Вот подпись:
func Unmarshal(data []byte, v interface{}) error
Сериализация простых типов
Вы можете легко сериализовать простые типы, например, используя пакет json. Результатом будет не полноценный объект JSON, а простая строка. Здесь int 5 сериализуется в байтовый массив [53], который соответствует строке «5».
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// Serialize int
var x = 5
bytes, err := json.Marshal(x)
if err != nil {
fmt.Println(«Can’t serislize», x)
}
fmt.Printf(«%v => %v, ‘%v’\n», x, bytes, string(bytes))
// Deserialize int
var r int
err = json.Unmarshal(bytes, &r)
if err != nil {
fmt.Println(«Can’t deserislize», bytes)
}
fmt.Printf(«%v => %v\n», bytes, r)
Output:
— 5 => [53], ‘5’
— [53] => 5
|
Если вы попытаетесь сериализовать неподдерживаемые типы, такие как функция, вы получите ошибку:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Trying to serialize a function
foo := func() {
fmt.Println(«foo() here»)
}
bytes, err = json.Marshal(foo)
if err != nil {
fmt.Println(err)
}
Output:
json: unsupported type: func()
|
Сериализация произвольных данных с помощью карт
Сила JSON заключается в том, что он может очень хорошо представлять произвольные иерархические данные. Пакет JSON поддерживает его и использует общий пустой интерфейс (interface {}) для представления любой иерархии JSON. Вот пример десериализации и последующей сериализации двоичного дерева, где каждый узел имеет значение int и две ветви, левую и правую, которые могут содержать другой узел или быть нулевыми.
JSON null эквивалентен Go nil. Как видно из выходных данных, json.Unmarshal()
успешно преобразовала большой двоичный объект JSON в структуру данных Go, состоящую из вложенной карты интерфейсов, и сохранила тип значения как int. Функция json.Marshal()
успешно сериализовала полученный вложенный объект в то же представление JSON.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// Arbitrary nested JSON
dd := `
{
«value»: 3,
«left»: {
«value»: 1,
«left»: null,
«right»: {
«value»: 2,
«left»: null,
«right»: null
}
},
«right»: {
«value»: 4,
«left»: null,
«right»: null
}
}`
var obj interface{}
err = json.Unmarshal([]byte(dd), &obj)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(«———\n», obj)
}
data, err = json.Marshal(obj)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(«———\n», string(data))
}
}
Output:
———
map[right:map[value:4
left:<nil>
right:<nil>]
value:3
left:map[left:<nil>
right:map[value:2
left:<nil>
right:<nil>]
value:1]]
———
{«left»:{
«left»:null,
«right»:{«left»:null,»right»:null,»value»:2},
«value»:1},
«right»:{«left»:null,
«right»:null,
«value»:4},
«value»:3}
|
Чтобы пройти общие карты интерфейсов, вам нужно использовать утверждения типа. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func dump(obj interface{}) error {
if obj == nil {
fmt.Println(«nil»)
return nil
}
switch obj.(type) {
case bool:
fmt.Println(obj.(bool))
case int:
fmt.Println(obj.(int))
case float64:
fmt.Println(obj.(float64))
case string:
fmt.Println(obj.(string))
case map[string]interface{}:
for k, v := range(obj.(map[string]interface{})) {
fmt.Printf(«%s: «, k)
err := dump(v)
if err != nil {
return err
}
}
default:
return errors.New(
fmt.Sprintf(«Unsupported type: %v», obj))
}
return nil
}
|
Сериализация структурированных данных
Работа со структурированными данными часто является лучшим выбором. Go предоставляет отличную поддержку для сериализации JSON в / из structs
через его теги struct
. Давайте создадим struct
которая соответствует нашему дереву JSON и более умной функции Dump()
которая ее печатает:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
type Tree struct {
value int
left *Tree
right *Tree
}
func (t *Tree) Dump(indent string) {
fmt.Println(indent + «value:», t.value)
fmt.Print(indent + «left: «)
if t.left == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.left.Dump(indent + » «)
}
fmt.Print(indent + «right: «)
if t.right == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.right.Dump(indent + » «)
}
}
|
Это здорово и намного чище, чем произвольный подход JSON. Но работает ли это? На самом деле, нет. Там нет ошибки, но наш объект дерева не заполняется JSON.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
jsonTree := `
{
«value»: 3,
«left»: {
«value»: 1,
«left»: null,
«right»: {
«value»: 2,
«left»: null,
«right»: null
}
},
«right»: {
«value»: 4,
«left»: null,
«right»: null
}
}`
var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
fmt.Printf(«- Can’t deserislize tree, error: %v\n», err)
} else {
tree.Dump(«»)
}
Output:
value: 0
left: <nil>
right: <nil>
|
Проблема в том, что поля дерева являются частными. Сериализация JSON работает только для открытых полей. Таким образом, мы можем сделать поля struct
общедоступными. Пакет json достаточно умен, чтобы прозрачно преобразовать строчные клавиши «value», «left» и «right» в соответствующие им имена полей верхнего регистра.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
type Tree struct {
Value int `json:»value»`
Left *Tree `json:»left»`
Right *Tree `json:»right»`
}
Output:
value: 3
left:
value: 1
left: <nil>
right:
value: 2
left: <nil>
right: <nil>
right:
value: 4
left: <nil>
right: <nil>
|
Пакет json автоматически игнорирует несопоставленные поля в JSON, а также приватные поля в вашей struct
. Но иногда вам может понадобиться отобразить определенные ключи в JSON на поле с другим именем в вашей struct
. Вы можете использовать struct
теги для этого. Например, предположим, что мы добавляем другое поле с именем «label» в JSON, но нам нужно сопоставить его с полем «Tag» в нашей структуре.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
type Tree struct {
Value int
Tag string `json:»label»`
Left *Tree
Right *Tree
}
func (t *Tree) Dump(indent string) {
fmt.Println(indent + «value:», t.Value)
if t.Tag != «» {
fmt.Println(indent + «tag:», t.Tag)
}
fmt.Print(indent + «left: «)
if t.Left == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.Left.Dump(indent + » «)
}
fmt.Print(indent + «right: «)
if t.Right == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.Right.Dump(indent + » «)
}
}
|
Вот новый JSON с корневым узлом дерева, помеченным как «root», правильно сериализованным в поле Tag и напечатанным в выходных данных:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
dd := `
{
«label»: «root»,
«value»: 3,
«left»: {
«value»: 1,
«left»: null,
«right»: {
«value»: 2,
«left»: null,
«right»: null
}
},
«right»: {
«value»: 4,
«left»: null,
«right»: null
}
}`
var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
fmt.Printf(«- Can’t deserislize tree, error: %v\n», err)
} else {
tree.Dump(«»)
}
Output:
value: 3
tag: root
left:
value: 1
left: <nil>
right:
value: 2
left: <nil>
right: <nil>
right:
value: 4
left: <nil>
right: <nil>
|
Написание обычного маршаллера
Вам часто захочется сериализовать объекты, которые не соответствуют строгим требованиям функции Marshal (). Например, вы можете сериализовать карту с помощью ключей int. В этих случаях вы можете написать собственный маршаллер / демаршаллер, реализовав интерфейсы Marshaler
и Unmarshaler
.
Примечание о правописании. В Go принято называть интерфейс одним методом, добавляя суффикс «er» к имени метода. Таким образом, несмотря на то, что более распространенным написанием является «Marshaller» (с двойным L), имя интерфейса просто «Marshaler» (одиночный L).
Вот интерфейсы Marshaler и Unmarshaler:
1
2
3
4
5
6
7
|
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
|
Вы должны создать тип при выполнении пользовательской сериализации, даже если вы хотите сериализовать встроенный тип или композицию встроенных типов, таких как map[int]string
. Здесь я определяю тип с именем IntStringMap
и реализую интерфейсы Marshaler
и Unmarshaler
для этого типа.
Метод MarshalJSON()
создает map[string]string
, преобразует каждый из своих собственных ключей int в строку и сериализует карту со строковыми ключами, используя стандартную json.Marshal()
.
01
02
03
04
05
06
07
08
09
10
|
type IntStringMap map[int]string
func (m *IntStringMap) MarshalJSON() ([]byte, error) {
ss := map[string]string{}
for k, v := range *m {
i := strconv.Itoa(k)
ss[i] = v
}
return json.Marshal(ss)
}
|
Метод UnmarshalJSON () делает прямо противоположное. Он десериализует массив байтов данных в map[string]string
а затем преобразует каждый строковый ключ в int и заполняет сам себя.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
func (m *IntStringMap) UnmarshalJSON(data []byte ) error {
ss := map[string]string{}
err := json.Unmarshal(data, &ss)
if err != nil {
return err
}
for k, v := range ss {
i, err := strconv.Atoi(k)
if err != nil {
return err
}
(*m)[i] = v
}
return nil
}
|
Вот как это использовать в программе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
m := IntStringMap{4: «four», 5: «five»}
data, err := m.MarshalJSON()
if err != nil {
fmt.Println(err)
}
fmt.Println(«IntStringMap to JSON: «, string(data))
m = IntStringMap{}
jsonString := []byte(«{\»1\»: \»one\», \»2\»: \»two\»}»)
m.UnmarshalJSON(jsonString)
fmt.Printf(«IntStringMap from JSON: %v\n», m)
fmt.Println(«m[1]:», m[1], «m[2]:», m[2])
Output:
IntStringMap to JSON: {«4″:»four»,»5″:»five»}
IntStringMap from JSON: map[2:two 1:one]
m[1]: one m[2]: two
|
Сериализация Enums
Перечисления Go могут быть довольно неприятными для сериализации. Идея написать статью о сериализации Go json возникла из-за вопроса, который мне спросил коллега о том, как сериализовать перечисления. Вот enum
Go. Константы Zero и One равны целым числам 0 и 1.
1
2
3
4
5
6
|
type EnumType int
const (
Zero EnumType = iota
One
)
|
Хотя вы можете думать, что это int, и во многих отношениях это так, вы не можете сериализовать его напрямую. Вы должны написать собственный маршалер / демаршалер. Это не проблема после последнего раздела. Следующие MarshalJSON()
и UnmarshalJSON()
будут сериализовать / десериализовать константы ZERO и ONE в / из соответствующих строк «Zero» и «One».
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func (e *EnumType) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
value, ok := map[string]EnumType{«Zero»: Zero, «One»: One}[s]
if !ok {
return errors.New(«Invalid EnumType value»)
}
*e = value
return nil
}
func (e *EnumType) MarshalJSON() ([]byte, error) {
value, ok := map[EnumType]string{Zero: «Zero», One:»One»}[*e]
if !ok {
return nil, errors.New(«Invalid EnumType value»)
}
return json.Marshal(value)
}
|
Давайте попробуем встроить этот EnumType
в struct
и сериализовать его. Основная функция создает EnumContainer
и инициализирует его именем «Uno» и значением нашей константы enum
ONE
, которая равна int 1.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
type EnumContainer struct {
Name string
Value EnumType
}
func main() {
x := One
ec := EnumContainer{
«Uno»,
x,
}
s, err := json.Marshal(ec)
if err != nil {
fmt.Printf(«fail!»)
}
var ec2 EnumContainer
err = json.Unmarshal(s, &ec2)
fmt.Println(ec2.Name, «:», ec2.Value)
}
Output:
Uno : 0
|
Ожидаемый результат: «Uno: 1», но вместо этого «Uno: 0». Что произошло? В коде маршала / демаршала нет ошибки. Оказывается, вы не можете вставлять перечисления по значению, если хотите их сериализовать. Вы должны вставить указатель на перечисление. Вот модифицированная версия, где она работает как положено:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type EnumContainer struct {
Name string
Value *EnumType
}
func main() {
x := One
ec := EnumContainer{
«Uno»,
&x,
}
s, err := json.Marshal(ec)
if err != nil {
fmt.Printf(«fail!»)
}
var ec2 EnumContainer
err = json.Unmarshal(s, &ec2)
fmt.Println(ec2.Name, «:», *ec2.Value)
}
Output:
Uno : 1
|
Вывод
Go предоставляет много опций для сериализации и десериализации JSON. Важно понять все входы и выходы пакета encoding / json, чтобы воспользоваться преимуществами этой возможности.
В этом уроке вы получите всю мощь, в том числе и сериализацию неуловимых перечислений Go.
Иди сериализуй некоторые объекты!