encoding стандартной библиотеки Go одиннадцать пакетов. Часть из них служит для конвертации двоичных данных в строку и обратно, остальные — для сериализации в популярные форматы. В этом уроке расскажем о пакете encoding/json, который обеспечивает конвертацию структур, слайсов и мап любой вложенности в формат JSON.encoding-пакеты стандартной библиотеки работают только с экспортируемыми полями структуры, имя которых начинается с заглавной буквы.json предоставляет набор методов для работы с JSON-форматом, который начинал свою историю как подмножество синтаксиса языка JavaScript, а затем стал одним из самых популярных форматов представления данных в иерархическом виде.// это пример на JavaScript, где JSON представляется частью кода
var user = {
"id": 10,
"name": "Gopher",
"items": [10, 20, 30]
}
console.log(user) encoding/json позволяет кодировать объект в слайс байт ([]byte) и декодировать обратно. В первую очередь рассмотрим функцию Marshal() для сериализация данных в формат JSON и функцию Unmarshal() для преобразования JSON в мапу, слайс или структуру.json.Marshal() рекурсивно проходит по объекту и возвращает его JSON-представление в виде слайса байт:func Marshal(v interface{}) ([]byte, error) | ТИП | ВО ЧТО ПРЕОБРАЗУЕТСЯ |
|---|---|
bool | JSON boolean |
float, int | JSON number |
string | JSON string в кодировке UTF-8 с экранированием специальных символов, например, добавлением обратного слеша перед двойными кавычками (" → \") |
[]byte | Base64-строка |
| структуры | JSON object |
| массивы и слайсы | JSON array |
map | JSON object с условием, что ключ — это int или string |
interface{} | JSON null для nil-значений или формат в соответствии с типом значения |
nil, они будут преобразованы в JSON null. Если они инициализированы и пусты, они конвертируются в {} или [] соответственно.Marshal() на примере простейшего HTTP-сервера. Допустим, компания организует IT-конференцию и составляет список гостей. На входе в зал сканер считывает идентификатор с QR-кода бейджика и отправляет запрос к серверу, который должен вернуть информацию о посетителе в JSON-формате:package main
import (
"encoding/json"
"net/http"
)
type Visitor struct {
ID int
Name string
Phones []string
Company string
}
var visitors = map[string]Visitor{
"1": {
ID: 1,
Name: "Guest",
Phones: []string{
`789-673-56-90`,
`612-934-77-23`,
}},
}
func JSONHandler(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get("id")
resp, err := json.Marshal(visitors[id])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
func main() {
http.ListenAndServe("localhost:8080", http.HandlerFunc(JSONHandler))
} http://localhost:8080/?id=1, то увидите такой результат:{"ID":1,"Name":"Guest","Phones":["789-673-56-90","612-934-77-23"],"Company":""} json. Так как в примере ни у одного из полей этот тег не был указан, ключи совпадают с именами полей. json:json:"name" — при таком значении ключ поля в JSON будет равен name.json:"-" — дефис заставляет пакет игнорировать поле. Если вам нужно, чтобы поле было экспортируемым, но не фигурировало в JSON-представлении, то при десериализации такое поле будет пропущено и сохранит прежнее значение.json:"name,omitempty" — здесь добавлен тег omitempty. Если значение поля равно нулевому значению для соответствующего типа (например, nil для указателя, пустая строка для string, 0 для int и так далее), то при сериализации поле будет пропущено, а JSON-ключ не будет создан.json-теги для структуры Visitor из примера выше:type Visitor struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Phones []string `json:"phones,omitempty"`
Company string `json:"company,omitempty"`
} {"id":1,"name":"Guest","phones":["789-673-56-90","612-934-77-23"]} company и изменились имена ключей. Если отправить запрос http://localhost:8080/?id=2, то результатом будет {"id":0}. omitempty стоит использовать с осторожностью. Если поле company отсутствует, то при обработке полученных данных другая программа может возвращать ошибку. А если значение этого поля равно пустой строке, то программа будет работать.Unmarshal().encoding/json содержит функцию Unmarshal(), которая выполняет десериализацию JSON-объекта:func Unmarshal(data []byte, v interface{}) error data — это данные в JSON-формате, а аргумент v — указатель на значение или переменную, куда будет записан результат десериализации.json.Marshal(), действуют и для обратной операции. При этом, если точного совпадения имени поля не найдено, поиск поля выполняется ещё раз, но уже без учёта регистра. Если поле структуры не найдено в JSON-объекте, функция оставит значение этого поля без изменений.JSONHandler таким образом, чтобы он обрабатывал POST-запросы и добавлял данные посетителя, переданные в JSON-формате:func JSONHandler(w http.ResponseWriter, req *http.Request) {
var id string
if req.Method == http.MethodPost {
var visitor Visitor
var buf bytes.Buffer
// читаем тело запроса
_, err := buf.ReadFrom(req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// десериализуем JSON в Visitor
if err = json.Unmarshal(buf.Bytes(), &visitor); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
id = strconv.Itoa(visitor.ID)
// добавляем в мапу
visitors[id] = visitor
} else {
id = req.URL.Query().Get("id")
}
resp, err := json.Marshal(visitors[id])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(resp)
} POST-запроса:func main() {
go func() {
time.Sleep(time.Second)
http.Post(`http://localhost:8080`, `application/json`,
// ключи указаны в разных регистрах, но данные сконвертируются правильно
bytes.NewBufferString(`{"ID": 10, "NaMe": "Gopher", "company": "Don't Panic"}`))
}()
http.ListenAndServe("localhost:8080", http.HandlerFunc(JSONHandler))
} http://localhost:8080/?id=7 браузер выведет:{"id":10,"name":"Gopher","company":"Don't Panic"} Marshal() генерирует JSON в одну строку. При обмене данными это скорее плюс — нет переводов строки, табуляции и лишних пробелов, то есть меньше объём передаваемой информации.Marshal() лучше заменить на MarshalIndent(). json.MarshalIndent() приводит результат обработки данных к привычному виду, разбивает на строки, добавляет префикс и отступ к каждому ключу:func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) json.Marshal(visitors[id]) на json.MarshalIndent(visitors[id], "", " "), то вывод будет таким:{
"id": 1,
"name": "Guest",
"phones": [
"789-673-56-90",
"612-934-77-23"
]
} encoding/json для пользовательского типа. По умолчанию за сериализацию и десериализацию данных отвечают интерфейсы json.Marshaler и json.Unmarshaler, которые реализованы для всех базовых типов, включая структуры.MarshalJSON() и UnmarshalJSON():type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
} Visitor.ID имеет тип int и преобразуется в число в JSON-формате. Предположим, надо сделать так, чтобы id в JSON был строкой. Для этого добавим свою реализацию интерфейсов json.Marshaler и json.Unmarshaler для типа Visitor:// MarshalJSON реализует интерфейс json.Marshaler.
func (v Visitor) MarshalJSON() ([]byte, error) {
// чтобы избежать рекурсии при json.Marshal, объявляем новый тип
type VisitorAlias Visitor
aliasValue := struct {
VisitorAlias
// переопределяем поле внутри анонимной структуры
ID string `json:"id"`
}{
// встраиваем значение всех полей изначального объекта (embedding)
VisitorAlias: VisitorAlias(v),
// задаём значение для переопределённого поля
ID: strconv.Itoa(v.ID),
}
return json.Marshal(aliasValue) // вызываем стандартный Marshal
} {"name":"Guest","phones":["789-673-56-90","612-934-77-23"],"id":"1"} UnmarshalJSON(), чтобы принимать POST-запросы с id в виде строки:// UnmarshalJSON реализует интерфейс json.Unmarshaler.
func (v *Visitor) UnmarshalJSON(data []byte) (err error) {
// чтобы избежать рекурсии при json.Unmarshal, объявляем новый тип
type VisitorAlias Visitor
aliasValue := &struct {
*VisitorAlias
// переопределяем поле внутри анонимной структуры
ID string `json:"id"`
}{
VisitorAlias: (*VisitorAlias)(v),
}
// вызываем стандартный Unmarshal
if err = json.Unmarshal(data, aliasValue); err != nil {
return
}
v.ID, err = strconv.Atoi(aliasValue.ID)
return
} MyType, где поле Value статически не задано и тип его значения зависит от значения поля Type:type (
// MyType использует динамическую типизацию своих полей
MyType struct {
// Type задаёт тип объекта поля Value
Type int `json:"type"`
// Value содержит объект типа {TypeA, TypeB}
Value interface{} `json:"value"`
}
// TypeA используется полем Value типа MyType при Type == 1
TypeA struct {
StrValue string `json:"str_value"`
}
// TypeB используется полем Value типа MyType при Type == 2
TypeB struct {
FloatValue float32 `json:"float_value"`
}
) func main() {
tOrig := MyType{
Type: 1,
Value: TypeA{
StrValue: "some_string",
},
}
tOrigJSON, _ := json.MarshalIndent(tOrig, "", " ")
fmt.Printf("tOrigJSON:\n%s\n", string(tOrigJSON))
tCopy := MyType{}
json.Unmarshal(tOrigJSON, &tCopy)
fmt.Printf("tCopy Go: %+v\n", tCopy)
fmt.Printf("tCopy.Value Go: %+v\n", tCopy.Value)
} tOrigJSON:
{
"type": 1,
"value": {
"str_value": "some_string"
}
}
tCopy Go: {Type:1 Value:map[str_value:some_string]}
tCopy.Value Go: map[str_value:some_string] json.Unmarshal() не знает, какому типу соответствует interface{} поля Value.json.Unmarshal() десериализует тип interface{} в map[string]interface{}. На практике такое поведение можно применить для десериализации любого JSON-объекта, не описывая предварительно его Go-представление. После десериализации объекта в map[string]interface{} вы получите ключи и значения первого уровня вложенности. Объект можно «разворачивать» и дальше для получения нужного ключа первого уровня, если использовать преобразование типов (type assertion).MyType интерфейс json.Unmarshaler — добавим метод UnmarshalJSON(data []byte) error. В методе укажем все возможные типы объектов, которые могут содержаться в поле Value:func (t *MyType) UnmarshalJSON(data []byte) error {
// чтобы избежать рекурсии при json.Unmarshal, объявляем новый тип
type MyTypeAlias MyType
// переопределяем поле внутри анонимной структуры
aliasValue := &struct {
*MyTypeAlias
Value json.RawMessage `json:"value"`
}{
// встраиваем указатель на целевой объект для заполнения его полей (кроме Value)
MyTypeAlias: (*MyTypeAlias)(t),
}
if err := json.Unmarshal(data, aliasValue); err != nil {
return err
}
// создаём экземпляр конкретного типа
switch t.Type {
case 1:
t.Value = &TypeA{}
case 2:
t.Value = &TypeB{}
}
if t.Value != nil {
// производим Unmarshal json.RawMessage значения
if err := json.Unmarshal(aliasValue.Value, t.Value); err != nil {
return fmt.Errorf("value of type (%T): unmarshal: %w", t.Value, err)
}
}
return nil
} tOrigJSON:
{
"type": 1,
"value": {
"str_value": "some_string"
}
}
tCopy Go: {Type:1 Value:0xc000014470}
tCopy.Value Go: &{StrValue:some_string} json.RawMessage — переопределение типа []byte. Функция json.Unmarshal() не выполняет автоматическую десериализацию полей этого типа, что даёт возможность по-разному обрабатывать эти значения в дальнейшем.MyTypeAlias переопределяет поле Value типом json.RawMessage, что позволяет десериализовать «сырой» JSON в конкретный тип (TypeA или TypeB).[]byte и его копирования в хранилище. Для этого используют энкодер и декодер.json.Encoder и json.Decoder читают и записывают результат сериализации в объект, реализующий интерфейс io.Writer:var buf bytes.Buffer
var v MyType
jsonEncoder := json.NewEncoder(&buf)
jsonEncoder.Encode(v)
jsonDecoder := json.NewDecoder(&buf)
jsonDecoder.Decode(&v) {...}. Остальные блоки будут десериализованы в следующей итерации.Body HTTP-запроса. В обработчике JSONHandler() тело запроса читалось методом ReadFrom(), а затем вызывалась функция Unmarshal(). Если заменить это решение на json.NewDecoder, то код обработчика сократится:func JSONHandler(w http.ResponseWriter, req *http.Request) {
var id string
if req.Method == http.MethodPost {
var visitor Visitor
if err := json.NewDecoder(req.Body).Decode(&visitor); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
id = strconv.Itoa(visitor.ID)
// добавляем в мапу
visitors[id] = visitor
}
// код ниже остался без изменений
// ...
} encoding/json.