четверг, 14 ноября 2019 г.

Работа с JSON в Golang

JSON (JavaScript Object Notation) - это простой формат обмена данными. Синтаксически это напоминает объекты и списки JavaScript. Он чаще всего используется для связи между веб-интерфейсами и программами JavaScript, работающими в браузере, но также используется и во многих других местах. Его домашняя страница, json.org, предоставляет четкое и краткое определение стандарта.

С пакетом json легко и быстро читать и записывать данные JSON из ваших программ Go.

Кодирование

Для кодирования данных в JSON используется функция Marshal.

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

Задав структуру данных Go, Message,

type Message struct {
    Name string
    Body string
    Time int64
}

и экземпляр сообщения

m := Message{"Alice", "Hello", 1294706395881547000}

мы можем получить JSON-кодированную версию m, используя json.Marshal:

b, err := json.Marshal(m)

Если все хорошо, err будет nil, а b будет []byte, содержащим JSON данные:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

Будут закодированы только структуры данных, которые могут быть представлены валидным JSON:

  • Объекты JSON поддерживают только строки в качестве ключей; чтобы кодировать Go тип map он должен иметь вид map[string]T (где T - любой тип Go, поддерживаемый пакетом json).
  • Channel, complex и func типы не могут быть закодированы.
  • Циклические структуры данных не поддерживаются; они приведут к попаданию Marshal в бесконечный цикл.
  • Указатели будут закодированы как значения, на которые они указывают (или 'null', если указатель равен nil).

Пакет json обращается только к экспортированным полям struct типов (те, которые начинаются с заглавной буквы). Поэтому в выводе JSON будут присутствовать только экспортированные поля структуры.

Декодирование

Для декодирования JSON данных используем функцию Unmarshal.

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

Сначала нужно создать место, где будут храниться декодированные данные

var m Message

и вызвать json.Unmarshal, передав ему []byte JSON данных и указатель на m

err := json.Unmarshal(b, &m)

Если b содержит допустимый JSON, который соответствует m, после вызова err будет nil и данные из b будут сохранены в структуре m, как если бы это было сделано с помощью присваивания, подобного следующему:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Как Unmarshal идентифицирует поля для хранения декодированных данных? Для заданного ключа JSON "Foo" Unmarshal просматривает поля структуры назначения, чтобы найти (в порядке предпочтения):

  • Экспортированное поле с тегом "Foo"
  • Экспортированное поле с именем "Foo"
  • Экспортированное поле с именем "FOO" или "FoO" или другое нечувствительное к регистру совпадение "Foo"

Что происходит, когда структура данных JSON не совсем соответствует типу Go?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal будет декодировать только те поля, которые он может найти в типе назначения. В этом случае будет заполнено только поле Name в m, а поле Food будет игнорироваться. Это поведение особенно полезно, когда вы хотите выбрать только несколько определенных полей из большого JSON-объекта. Это также означает, что Unmarshal не затронет любые неэкспортированные поля в структуре назначения.

Но что, если вы заранее не знаете структуру данных JSON?

Универсальный JSON с interface{}

Тип interface{} (пустой интерфейс) описывает интерфейс с нулевыми методами. Каждый тип Go реализует как минимум ноль методов и поэтому удовлетворяет пустому интерфейсу.

Пустой интерфейс служит общим типом контейнера:

var i interface{}
i = "a string"
i = 2011
i = 2.777

Утверждение типа обращается к подлежащему конкретному типу:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

Или, если базовый тип неизвестен, переключатель типа определяет тип:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i ни один из типов выше
}

Пакет json использует значения map[string]interface{} и []interface{} для хранения произвольных объектов и массивов JSON; он с радостью разархивирует любой допустимый большой бинарный объект JSON (blob) в простое значение interface{}. Конкретные типы Go по умолчанию:

  • bool для логических выражений JSON
  • float64 для JSON чисел
  • string для JSON строк
  • nil для JSON null

Декодирование произвольных данных

Рассмотрим JSON данные, хранящиеся в переменной b:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

Не зная структуры этих данных, мы можем декодировать их в значение interface{} с помощью Unmarshal:

var f interface{}
err := json.Unmarshal(b, &f)

В этот момент значением Go в f будет карта, ключи которой являются строками, а сами значения хранятся как пустые значения интерфейса:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

Чтобы получить доступ к этим данным, мы можем использовать утверждение типа для доступа к подлежащему map[string]interface{}:

m := f.(map[string]interface{})

Затем мы можем перебрать карту с помощью оператора range и использовать переключатель типа для доступа к ее значениям в качестве их конкретных типов:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

Таким образом, вы можете работать с неизвестными JSON данными, сохраняя при этом преимущества безопасности типов.

Ссылочные типы

Определим тип Go, который будет содержать данные из предыдущего примера:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

Демонстрация этих данных в значение FamilyMember работает, как и ожидалось, но если мы посмотрим внимательнее, то увидим, что произошла замечательная вещь. С помощью оператора var мы выделили структуру FamilyMember, а затем предоставили указатель на это значение для Unmarshal, но в то время поле Parents было нулевым значением среза. Чтобы заполнить поле Parents, Unmarshal выделил новый срез за кулисами. Это типично для того, как Unmarshal работает с поддерживаемыми ссылочными типами (указатели, срезы и карты).

Рассмотрим конвертирование в эту структуру данных:

type Foo struct {
    Bar *Bar
}

Если бы в объекте JSON было поле Bar, Unmarshal выделил бы новый Bar и заполнил его. Если нет, Bar будет оставлен как нулевой указатель.

Из этого вытекает полезный шаблон: если у вас есть приложение, которое получает несколько различных типов сообщений, вы можете определить структуру "получателя", например:

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

и отправляющая сторона может заполнить поле Cmd и/или поле Msg объекта JSON верхнего уровня, в зависимости от типа сообщения, которое они хотят передать. Unmarshal при декодировании JSON в структуру IncomingMessage будет выделять только структуры данных, присутствующие в JSON данных. Чтобы узнать, какие сообщения обрабатывать, программисту просто нужно проверить, что Cmd или Msg не ноль.

Потоковые кодировщики и декодеры

Пакет json предоставляет типы Decoder и Encoder для поддержки обычной операции чтения и записи потоков данных JSON. Функции NewDecoder и NewEncoder оборачивают типы интерфейсов io.Reader и io.Writer.

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

Вот пример программы, которая читает серию объектов JSON из стандартного ввода, удаляет все, кроме поля Name, из каждого объекта, а затем записывает объекты в стандартный вывод:

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

Из-за повсеместного распространения программ чтения и записи эти типы кодировщиков и декодеров могут использоваться в широком диапазоне сценариев, таких как чтение и запись в соединения HTTP, веб-сокеты или файлы.

Пример использования тегов в структуре

Использование тегов в структуре кодируемой в JSON позволяет получить названия полей в результирующем JSON, отличающиеся от названия полей в структуре. В следующем примере в результирующем JSON поле BrandID будет выглядеть как brand_id:

package main

import (
 "encoding/json"
 "fmt"
)

type Item struct {
 ID      uint   `json:"id"`
 Title   string `json:"title"`
 BrandID uint   `json:"brand_id"`
}

func main() {

 item := Item{ID: 1, Title: "Car", BrandID: 1}
 jitem, err := json.Marshal(item)
 if err != nil {
  fmt.Println(err.Error())
  return
 }
 fmt.Println(string(jitem))
}

Вывод:

{"id":1,"title":"Car","brand_id":1}

Запустить пример в песочнице play.golang.org


Читайте также:


1 комментарий:

Darwin комментирует...

Спасибо за пост, очень сложно было понять как работать с Json, особенно после прихода в Go с Ruby.
Ошибки связанные с json иногда настолько непонятные, что сложно отлаживать, пример:
invalid character 'a' in literal true (expecting 'r')

Отправить комментарий