Когда я впервые работал над библиотекой gocoinex, мне потребовалась некоторая пользовательская настройка процесса размаршалинга JSON, и я хочу поделиться с вами своими наработками здесь, но перед этим мы сделаем обзор того, как мы можем размаршалить JSON в golang.
Это примеры из реального мира. (API биржи coinex)
Сначала мы делаем запрос следующим образом:
raw_response, _ := http.Get("https://api.coinex.com/v1/market/list")
И получим json-объект следующего вида:
{
"code": 0,
"data": [
"LTCBCH",
"ETHBCH",
"ZECBCH",
"DASHBCH"
],
"message": "Ok"
}
Но мы должны разобрать его,
Есть разные способы разобрать его,
Вы можете использовать функции NewDecoder или Unmarshal из пакета json,
и вы можете декодировать его в struct
или в map[string]interface{}
.
Это зависит от ваших предпочтений, но в данном случае я предпочитаю комбинацию NewDecoder и struct.
Итак, мы должны создать структуру, подобную этой:
type AllMarketList struct {
Code int `json:"code"`
Message string `json:"message"`
Data []string
}
Также мы можем иметь встроенные структуры, например, разобьем последнюю структуру на две:
type GeneralResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
type AllMarketList struct {
GeneralResponse
Data []string
}
И нет никакой разницы.
Наконец, используйте NewDecoder для декодирования raw_response в структуру AllMarketList:
var allMarketList AllMarketList
json.NewDecoder(raw_response.Body).Decode(&allMarketList)
Завершенный код
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type AllMarketList struct {
Code int `json:"code"`
Message string `json:"message"`
Data []string
}
func main() {
raw_response, _ := http.Get("https://api.coinex.com/v1/market/list")
var allMarketList AllMarketList
if err := json.NewDecoder(raw_response.Body).Decode(&allMarketList); err != nil {
fmt.Println(err)
}
defer raw_response.Body.Close()
fmt.Printf("%+vn", allMarketList)
}
Пример второй
Представьте, что у нас есть json следующего вида:
{
"code": 0,
"data": {
"date": 1513865441609, # server time when returning
"ticker": {
"open": "10", # highest price
"last": "10.00", # latest price
"vol": "110" # 24H volume
}
},
"message" : "Ok"
}
Мы собираемся улучшить некоторые вещи в процессе декодирования.
- В данном случае временная метка Unix не может быть разобрана, мы должны предоставить ей возможность.
- Мы хотим удалить ключ «ticker» и получить доступ к «open», «last», «vol» непосредственно из «data».
- «last» должен быть экспортирован в поле с именем «Close»! (то же самое происходит с «vol» и «Volume».
- «open», «last», «vol» должны быть float, а не string, но мы оставим их для следующего примера.
Проблемы 1 и 2 могут быть решены путем реализации метода UnmarshalJSON на любой структуре, которую мы хотим декодировать.
Проблема 3 будет легко решена с помощью тегов json. (Я упомянул об этом в коде ниже).
Наша конечная структура должна выглядеть следующим образом:
// Final struct
type SingleMarketStatistics struct {
Code int `json:"code"`
Message string `json:"message"`
Data TickerData
}
// Inner struct that we should implement to solve problem 2
type TickerData struct {
ServerTime CTime `json:"date"` // CTime is short for CustomTime
Open float64 `json:"open"`
Close float64 `json:"last"` // Different Attribute name and tag name
Volume float64 `json:"vol"` // Different Attribute name and tag name
}
// Custome time
// Inner struct that we should implement to solve problem 1
type CTime struct {
time.Time
}
Реализация пользовательского времени
func (t *CTime) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" || string(data) == `""` {
return nil
}
// Fractional seconds are handled implicitly by Parse.
i, err := strconv.ParseInt(string(data), 10, 64)
update := time.UnixMilli(i)
*t = CTime{update}
return err
}
И мы больше не получаем ошибку!
Этот метод будет автоматически использоваться (благодаря интерфейсам!) всякий раз, когда мы захотим декодировать время в CTime!
Реализация пользовательских данных
func (t *TickerData) UnmarshalJSON(data []byte) error {
if string(data) == "null" || string(data) == `""` {
return nil
}
// This is how this json really looks like.
var realTicker struct {
ServerTime CTime `json:"date"`
Ticker struct {
// tags also can be omitted when we're using UnmarshalJSON.
Open string `json:"open"`
Close string `json:"last"`
Volume string `json:"vol"`
} `json:"ticker"`
}
// Unmarshal the json into the realTicker struct.
if err := json.Unmarshal(data, &realTicker); err != nil {
return err
}
// Set the fields to the new struct,
// with any shape it has,
// or anyhow you want.
*t = TickerData{
ServerTime: realTicker.ServerTime,
Open: realTicker.Ticker.Open,
Close: realTicker.Ticker.Close,
Volume: realTicker.Ticker.Volume,
}
return nil
}
Теперь просто используйте NewDecoder, как и раньше, никаких изменений не требуется.
var singleMarketStatistics SingleMarketStatistics
json.NewDecoder(raw_response.Body).Decode(&allMarketList)
Пример третий
Представьте себе JSON следующего вида:
{
"asks": [ // This is a array of asks
[ // This is a array of ONE ask
"10.00", // Price of ONE ask
"0.999", // Amount of ONE ask
]
],
"bids": [ // Same structure as asks
[
"10.00",
"1.000",
]
]
}
Как совершенно ясно, непрофессиональным способом, мы должны декодировать «asks» в [][]string
и получить доступ к цене первого запроса следующим образом asks[0][0]
и сумме asks[0][1]
.
Кто помнит, что 0 — это цена, а 1 — сумма? Что из этого было? 😄
Поэтому мы будем управлять ими в методе UnmarshalJSON.
Также мы решим проблему 4 из предыдущего примера, которая существует и здесь.
type BidAsk struct {
// Tags are not needed.
Price float64 `json:"price"` // Bid or Ask price
Amount float64 `json:"amount"` // Bid or Ask amount
}
func (t *BidAsk) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" || string(data) == `""` {
return nil
}
// Unmarshal to real type.
var bisask []string
if err := json.Unmarshal(data, &bisask); err != nil {
return err
}
// Change value type from string to float64.
price, err := strconv.ParseFloat(bisask[0], 64)
if err != nil {
return err
}
amount, err := strconv.ParseFloat(bisask[1], 64)
if err != nil {
return err
}
// Map old structure to new structure.
*t = BidAsk{
Price: price,
Amount: amount,
}
return err
}
type MarketDepth struct {
Asks []BidAsk `json:"asks"` // Ask depth
Bids []BidAsk `json:"bids"` // Bid depth
}
Опять же, мы просто используем:
var marketDepth MarketDepth
json.NewDecoder(raw_response.Body).Decode(&marketDepth)
И наслаждаемся красотой результата:
for i, ask := range data.Data.Asks {
fmt.Printf("Ask %vn", i)
fmt.Printf(" Price: %vn", ask.Price) // here is the beauty
fmt.Printf(" Amount: %vn", ask.Amount) // here is the beauty
fmt.Println()
}
for i, bid := range data.Data.Bids {
fmt.Printf("Bid %vn", i)
fmt.Printf(" Price: %vn", bid.Price) // here is the beauty
fmt.Printf(" Amount: %vn", bid.Amount) // here is the beauty
fmt.Println()
}