使用官方库在golang中表示json的三种方法


以下方法均只使用了encoding/json这个库,但事实上业界还有很多很优秀的JSON解析库,也对应着有不同表示JSON的方法。本文只描述使用官方库时可用的3种方式。

解析为结构体

第一步当然是设计JSON对应的结构体。使用类似 json-to-go这样的工具可以直接用JSON文件生成对应的结构体。

接着用json.Unmarshal解析到对应的结构体即可。

这种方式当然只适合于JSON格式比较固定的情形,如果JSON中的字段名会变化,结构会变化,这种方法就不能正确地表达JSON的内容了。但是,这个特性在某些场景也挺有用,比如处理POST请求时,程序可以合法接受的JSON格式一般是固定的。把用户提交的JSON内容解析到一个struct上,这样可以自动过滤掉用户提交的JSON文件中不需要的字段。

解析为map[string]interface{}

这种方式非常通用,任何JSON结构都可以解析为map[string]interface{}。给定JSON的文本后,encoding/json这个库实际上还是会将JSON的内容按类型正确解析,该是int的就是int,但是,都存储在interface{}类型的变量中。这样,一个变量可以容纳JSON的所有内容,但每次取用其中的一部分内容时,都要首先将interface{}转成你需要的变量类型。事实上,虽然可以表示任何JSON,但要取用JSON的部分内容,还是要首先知道JSON的这部分的类型。举个例子:

package main

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

// 预定义的JSON文件
var jsontext = []byte(`
{
	"code": 200,
	"info": [{
		"host": "wrong.wang",
		"spend": 1.23,
		"author": {
			"name": "Wrong Wang"
		}
	}]
}
`)

func main() {
	var m map[string]interface{}
	// 解析
	if err := json.Unmarshal(jsontext, &m); err != nil {
		log.Fatal(err)
	}
	// 内部已经知道了对应的类型,只是把数据存在interface{}容器中
	fmt.Println(m)

	code, ok := m["code"]
	if !ok {
		log.Fatal("'code' field not found")
	}
	// 数字一定被解析为float64,尽管这里200可以被解析为int类型
	switch code.(type) {
	case int64:
		fmt.Println("unmarshal  `code` into int64", code)
	case float64:
		fmt.Println("unmarshal  `code` into float64", code)
	case int:
		fmt.Println("unmarshal  `code` into int", code)
	case string:
		fmt.Println("unmarshal  `code` into string", code)
	default:
		fmt.Println("unmarshal what type??")
	}

	// 为了代码简洁,假设这些类型都已知,
	// 实际使用时,每一步类型转换,每一步map取值,都需要检查是否报错。
	infoi := m["info"]
	info := infoi.([]interface{})
	infom := info[0].(map[string]interface{})
	fmt.Println(infom["host"])
}

上面的代码可以在 The Go Playground中查看。

上面这份代码运行结果如下:

map[code:200 info:[map[author:map[name:Wrong Wang] host:wrong.wang spend:1.23]]]
unmarshal  `code` into float64 200
wrong.wang

总的来说,解析为map[string]interface{}类型后,读取任何一个字段都需要检查,都需要类型转换。JSON中类型和Go类型对应关系如下:

JSON类型 Golang类型
boolean bool
numbers float64
string string
nil null
array []interface{}
map map[string]interface{}

混合以上两种表达

上面两种表示法实际上已经根据JSON类型转换到了Go中的类型,即已经解析了。还有一种暂不解析的JSON的表达方式:解析为map[string]json.RawMessagejson.RawMessage 是原始未解析的JSON值,它实现了json.Marshalerjson.Unmarshaler的方法,可以用来延迟解析。即真正要使用对应字段的内容时再继续解析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 interface{}
		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)
	}
}

这种方式适合从一个大JSON文件中提取部分结构,比较灵活方便。同时因为是用时才解析,相比于前两种方法也有一定的性能优势。