数字转字符串
因为前端 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)
}
}
}