继续创造,加速成长!这是我参与「日新方案 10 月更文应战」的第1天,点击检查活动概况

json 作为一种通用的编解码协议,可阅览性上比 thrift,protobuf 等协议要好一些,同时编码的 size 也会比 xml 这类协议要小,在市面上用的十分多。甚至在许多事务上,咱们的线上实例消耗最大的部分便是 json 的序列化和反序列化。这也是为什么许多 Gopher 会致力于研究怎样最有效地优化这个进程。

今天咱们来学习一个 Golang 官方 json 库提供了一个经典才能:RawMessage。

什么是序列化

首先咱们考虑一下所谓序列化指的是什么呢?

参阅 json 包中 Marshaler 和 Unmarshaler 两个接口界说:

// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

序列化,也便是 Marshal,需要将一种类型转换为一个字节数组,也便是这儿接口返回值的 []byte

// Unmarshaler is the interface implemented by types
// that can unmarshal a JSON description of themselves.
// The input can be assumed to be a valid encoding of
// a JSON value. UnmarshalJSON must copy the JSON data
// if it wishes to retain the data after returning.
//
// By convention, to approximate the behavior of Unmarshal itself,
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

而反序列化,则是序列化的逆进程,接收一个字节数组,转换为方针的类型值。

事实上假如你对自界说的类型完成了上面两个接口,调用 json 包的 json.Marshal 以及 json.Unmarshal 函数时就会执行你的完成。

简言之,实质上看,序列化便是将一个 object 转换为字节数组,即 []byte 的进程。

RawMessage

RawMessage is a raw encoded JSON value. It implements Marshaler and Unmarshaler and can be used to delay JSON decoding or precompute a JSON encoding.

RawMessage 具体来讲是 json 库中界说的一个类型。它完成了 Marshaler 接口以及 Unmarshaler 接口,以此来支撑序列化的才能。留意上面咱们引用 官方 doc 的说明。咱们直接来看看源码中的完成:

// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
	if m == nil {
		return []byte("null"), nil
	}
	return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
	if m == nil {
		return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
	}
	*m = append((*m)[0:0], data...)
	return nil
}
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)

十分直接,其实 RawMessage 底层便是一个 []byte。序列化时是直接把自己 return 回去了。而反序列化时则是把入参的 []byte 复制一份,写入自己的内存地址即可。

有意思了,前一节咱们提到过,序列化后产出的原本便是一个 []byte,那为什么还要专门再搞一个 RawMessage 出来,有什么效果呢?

没错,RawMessage 其实人如其名,代表的便是一个终态。什么意思呢?我原本便是个字节数组,那么假如你要对我进行序列化,就不需要什么本钱,直接把我这个字节数组拿过去即可。假如要反序列化,没事,你直接把原来的字节数组拿到就够了。

这便是 Raw 的含义,原来是什么样,现在便是什么样。原样拿过来即可。

这儿参照 Using Go’s json.RawMessage 的经典解释。

We can think of the raw message as a piece of information that we decide to ignore at the moment. The information is still there but we choose to keep it in its raw form — a byte array.

咱们能够把 RawMessage 看作是一部分能够暂时疏忽的信息,今后能够进一步去解析,但此时不必。所以,咱们保留它的原始形式,仍是个字节数组即可。

运用场景

软件开发中,咱们经常说不要过度设计,好的代码应当有清晰的运用场景,并且能高效地处理一类问题,而不是在想象和概念上造出来一个未经过验证的空中楼阁。

那么 RawMessage 是不是这样一个空中楼阁呢?其实并不是。

咱们能够将其当做一个【占位符】。想象一下,咱们给某种事务场景界说了一个通用的 model,其中部分数据需要在不同场景下对应不同的结构体。这个时分怎样 Marshal 成字节数组,存入数据库,以及读出数据,复原出 model 呢?

咱们就能够将这个可变的字段界说为 json.RawMessage,利用它适配万物的才能来进行读写。

复用预核算的 json 值

package main
import (
	"encoding/json"
	"fmt"
	"os"
)
func main() {
	h := json.RawMessage(`{"precomputed": true}`)
	c := struct {
		Header *json.RawMessage `json:"header"`
		Body   string           `json:"body"`
	}{Header: &h, Body: "Hello Gophers!"}
	b, err := json.MarshalIndent(&c, "", "\t")
	if err != nil {
		fmt.Println("error:", err)
	}
	os.Stdout.Write(b)
}

这儿 c 是咱们暂时界说的结构体,body 是清晰的一个字符串,而 header 是可变的。

还记得么?RawMessage 实质是个 []byte,所以咱们能够用

json.RawMessage(`{"precomputed": true}`)

来将一个字符串转换为 RawMessage。随后对其进行 Marshal,输出的成果如下:

{
	"header": {
		"precomputed": true
	},
	"body": "Hello Gophers!"
}

发现了么?

这儿 "precomputed": true 跟咱们构造的 RawMessage 是如出一辙的,所以对应到第一个才能:在序列化时运用一个预先核算好的 json 值。

延迟解析 json 结构

package main
import (
	"encoding/json"
	"fmt"
	"log"
)
func main() {
	type Color struct {
		Space string
		Point json.RawMessage // delay parsing until we know the color space
	}
	type RGB struct {
		R uint8
		G uint8
		B uint8
	}
	type YCbCr struct {
		Y  uint8
		Cb int8
		Cr int8
	}
	var j = []byte(`[
	{"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}},
	{"Space": "RGB",   "Point": {"R": 98, "G": 218, "B": 255}}
]`)
	var colors []Color
	err := json.Unmarshal(j, &colors)
	if err != nil {
		log.Fatalln("error:", err)
	}
	for _, c := range colors {
		var dst any
		switch c.Space {
		case "RGB":
			dst = new(RGB)
		case "YCbCr":
			dst = new(YCbCr)
		}
		err := json.Unmarshal(c.Point, dst)
		if err != nil {
			log.Fatalln("error:", err)
		}
		fmt.Println(c.Space, dst)
	}
}

这儿的比如其实更典型。Color 中的 Point 可能存在两种结构描绘,一种是 RGB,另一种是 YCbCr,而咱们对应到底层存储,又希望能复用,这是十分常见的。

所以,这儿采用了【两级反序列化】的战略:

  • 第一级,解析出来公共字段,利用 json.RawMessage 延迟这部分差异字段的解析。
  • 第二级,依据现已解析出来的字段(一般是有类似 type 的语义),判别再次反序列化时要运用的结构,基于 json.RawMessage 再次 Unmarshal,拿到最终的数据。

上面的示例输出成果如下:

YCbCr &{255 0 -10}
RGB &{98 218 255}

总结

json 提供的 RawMessage 是直接暴露了底层的 []byte 作为交互凭证,它能够被内嵌在各种结构体中。作为不可变的字段类型的 placeholder,延迟解析。相较于 string 类型效率更高。从完成上看十分简单,仅仅封装了一层字节数组的交互,我们能够放心运用。