Go json使用进阶技巧

1 篇文章 0 订阅

 

数字转字符串

因为前端 js 对 int64 的处理可能会因为溢出导致无法准确处理,因此我们期望可以返回字符串类型。

package main

type A struct {
    B int `json:",string"`
}

定制Marshaler/Unmarshal

 

在很多 case 中,我们需要对结构体进行一些定制,比如枚举类型和 string 的转换,比如一些属性的预处理,这个时候我们就需要实现自己的Marshal/Unmarshal方法了。 举一个很常见的例子,在 web 开发中,数据的 ID 可能我们并不希望暴露给外界,因此希望可以做一个转换。通常情况下,我们要定义两个结构体,data model 一个,web model 一个,中间通过额外的方法来做转换,有多少个结构对就需要多少个转换函数,实在是令人郁闷。

那么,对于上面提到的棘手的问题,有没有优雅而又简单的方法解决呢?答案是肯定的,那就是自定义实现Marshal/Unmarshal方法的类型。

 

废话不多说,直接上代码 

ackage base

import (
	"encoding/json"
	"strings"

	"github.com/golang/glog"
	"github.com/speps/go-hashids"
	"golang.org/x/xerrors"
)

var seed *hashids.HashID

func init() {
	Init("golang-advanced-json")
}

func Init(salt string) {
	idData := hashids.NewData()
	idData.Salt = salt
	var err error
	seed, err = hashids.NewWithData(idData)
	if err != nil {
		glog.Fatalf("initialize hash id failed, %s", err)
	}
}

type ID int64

func (i ID) MarshalText() (text []byte, err error) {
	if i < 0 {
		return nil, xerrors.Errorf("err: ID(%d) must greater than 0", i)
	}
	str, err := seed.EncodeInt64([]int64{int64(i)})
	if err != nil {
		return nil, err
	}
	return []byte(str), nil
}

func (i *ID) UnmarshalText(text []byte) error {
	is, err := seed.DecodeInt64WithError(string(text))
	if err != nil {
		return xerrors.Errorf("unmarshal id failed, %w", err)
	}
	if len(is) != 1 {
		return xerrors.Errorf("bad unmarshal id length, %d", len(is))
	}
	*i = ID(is[0])
	return nil
}

func (i ID) MarshalJSON() ([]byte, error) {
	text, err := i.MarshalText()
	if err != nil {
		return nil, err
	}
	return json.Marshal(string(text))
}

func (i *ID) UnmarshalJSON(data []byte) error {
	var text string
	err := json.Unmarshal(data, &text)
	if err != nil {
		return err
	}
	return i.UnmarshalText([]byte(text))
}

func ParseIDList(str string) ([]ID, error) {
	items := strings.Split(str, ",")
	ids := make([]ID, 0, len(items))
	for _, item := range items {
		id := new(ID)
		err := id.UnmarshalText([]byte(item))
		if err != nil {
			continue
		}
		ids = append(ids, *id)
	}
	return ids, nil
}

func ParseInt64List(str string) ([]int64, error) {
	ids, err := ParseIDList(str)
	if err != nil {
		return nil, err
	}
	int64s := make([]int64, 0, len(ids))
	for _, id := range ids {
		int64s = append(int64s, int64(id))
	}
	return int64s, nil
}

func FormatIDList(ids []ID) (string, error) {
	items := make([]string, 0, len(ids))
	for _, id := range ids {
		item, err := id.MarshalJSON()
		if err != nil {
			continue
		}
		items = append(items, string(item))
	}
	return strings.Join(items, ","), nil
}

懒惰解析/编码

有些情况下,我们不能第一时间来判断文档的类型,就无法通过定义结构体来处理文档了,如果使用map[string]interface{},则会损失类型检查,因此我们希望可以通过文档中某些固定的 key,来解析不固定的 key

package main

type MessageType int

const (
	MessageTypeText MessageType = iota + 1
)

type Message struct {
	MessageType  MessageType     `json:"messageType"`
	MessageId    int64           `json:"messageId"`
	MessageOwner int64           `json:"messageOwner"`
	Payload      json.RawMessage `json:"payload"`
}

type TextMessage struct {
	Text string `json:"text"`
}

type ImageMessage struct {
	Uri string `json:"uri"`
}

我们可以先解析到Message中,然后根据MessageType进行二次解析,这样,既能解析动态文档,也可以保证类型检查不丢失。

保留长整型

默认的 json 实现中,如果一个数据是长整型,并且对应的 go 中的数据类型是 interface 的话,就会产生解析成 float64 的问题,这个是绝对不能接受的。可以通过如下办法来解决:

 

package main

import "encoding/json"

func main() {
    d := json.NewDecoder([]byte(`{"a": 1234567890987654321}`))
    d.UseNumber()
    res := make(map[string]interface{})
    d.Decode(&res)
}

数据库查询时间字段为null,怎么处理?想统一给前端时间戳,怎么办?直接上代码,其他类型同理

package nulls

import (
	"database/sql/driver"
	"encoding/json"
	"time"
)

// Time replaces sql.NullTime with an implementation
// that supports proper JSON encoding/decoding.
type Time struct {
	Time  time.Time
	Valid bool // Valid is true if Time is not NULL
}

// Interface implements the nullable interface. It returns nil if
// the Time is not valid, otherwise it returns the Time value.
func (ns Time) Interface() interface{} {
	if !ns.Valid {
		return nil
	}
	return ns.Time
}

// NewTime returns a new, properly instantiated
// Time object.
func NewTime(t time.Time) Time {
	return Time{Time: t, Valid: true}
}

// Scan implements the Scanner interface.
func (ns *Time) Scan(value interface{}) error {
	ns.Time, ns.Valid = value.(time.Time)
	return nil
}

// Value implements the driver Valuer interface.
func (ns Time) Value() (driver.Value, error) {
	if !ns.Valid {
		return nil, nil
	}
	return ns.Time, nil
}

// MarshalJSON marshals the underlying value to a
// proper JSON representation.
func (ns Time) MarshalJSON() ([]byte, error) {
	if ns.Valid {
		return json.Marshal(ns.Time)
	}
	return json.Marshal(nil)
}

// UnmarshalJSON will unmarshal a JSON value into
// the propert representation of that value.
func (ns *Time) UnmarshalJSON(text []byte) error {
	ns.Valid = false
	txt := string(text)
	if txt == "null" || txt == "" {
		return nil
	}

	t := time.Time{}
	err := t.UnmarshalJSON(text)
	if err == nil {
		ns.Time = t
		ns.Valid = true
	}

	return err
}

// UnmarshalText will unmarshal text value into
// the propert representation of that value.
func (ns *Time) UnmarshalText(text []byte) error {
	return ns.UnmarshalJSON(text)
}

 如何使用

package main

type A struct {
    B nulls.Time `json:"b"`
}

自定义Tag标签

主要使用反射原理,和json包关系倒不大,但是经常需要在json解析后设置一些默认值等等。还是上代码

此方法可以在jsonUnmarshal后为零值的字段赋值为default标签内的值。其他功能道理相同。

package json

import (
	"bytes"
	"encoding/json"
	"reflect"
	"strconv"
)

const (
	defaultTagName = "default"
)
func setDefaultValueWithEmpty(val interface{}) {
	// TypeOf returns the reflection Type that represents the dynamic type of variable.
	// If variable is a nil interface value, TypeOf returns nil.
	t := reflect.TypeOf(val).Elem()
	v := reflect.ValueOf(val).Elem()
	for i := 0; i < t.NumField(); i++ {
		if _, exists := t.Field(i).Tag.Lookup(defaultTagName); !exists {
			continue
		}

		if v.Field(i).IsZero() {
			tagVal := t.Field(i).Tag.Get(defaultTagName)
			setValue(v.Field(i), tagVal)
		}
	}
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值