两个例子说明 JSON 字符串的反序列化

写在前面

看过《如何使 Kubernetes 中的应用日志被收集得又快又稳》的朋友应该知道,我最近在做云平台的日志收集方案及其工程实施。

不得不说,做基础设施建设对开发者的考验还是非常大的,技术方案中的每一个细节都变得非常重要,忽略某个细节可能就意味着方案中选型的改进甚至重新选型。至少在工程实施的过程中,有那么几天时间里我都是在这种担惊受怕中度过的。

当然,带有思考的付出总会有收获的,不仅仅对于团队如此(项目收益),对于个人来说亦如此(复习旧知识、学习新知识)。本文就借此机会复习一下旧知识——Go 反序列化 JSON 字符串的两种常见用法——为下一篇文章(关于 Go 标准库对 JSON 的处理效率)做铺垫。

适用人群

入门——初级√——中级——高级;本文适应初级及以上。

Go 反序列化 JSON 字符串的两种常见用法

反序列化 JSON 字符串,一般是为了能够获取得到 JSON 字符串中的信息(这句是废话)。比如我们希望通过一种便捷的方式获取某个 key 对应的值(value),从而用它完成接下来的代码逻辑。

那么这种“便捷的方式”是什么样的方式呢?

先看一个 JSON 字符串

// text-content-of-json JSON 字符串,一般的日志中会是这种格式
{"name":"chalvern.github.io","full_name":"chalvern/chalvern.github.io","private":false,"owner":{"login":"chalvern","html_url":"https://github.com/chalvern"},"html_url":"https://github.com/chalvern/chalvern.github.io","description":"jingwei.link blog"}

// string-content 另一种 JSON 字符串,
// 大家可以思考一下与上面内容的区别(怀疑很多人分不清楚)
// 其实 text-content-of-json 与 string-content 是
// 代表同样 JSON 内容的不同表现形式(后者有转义符而已)
// 他们反序列化后会得到同样的结果(下一篇文章会再次提到此知识点)
"{\"name\":\"chalvern.github.io\",\"full_name\":\"chalvern/chalvern.github.io\",\"private\":false,\"owner\":{\"login\":\"chalvern\",\"html_url\":\"https://github.com/chalvern\"},\"html_url\":\"https://github.com/chalvern/chalvern.github.io\",\"description\":\"jingwei.link blog\"}"

// echo ```text-content-of-json``` | python -m json.tool
// 可以得到下面 Pretty 后的 JSON 格式
// Pretty 样式给人看起来比较舒服
{
  "name": "chalvern.github.io",
  "full_name": "chalvern/chalvern.github.io",
  "private": false,
  "owner": {
    "login": "chalvern",
    "html_url": "https://github.com/chalvern"
  },
  "html_url": "https://github.com/chalvern/chalvern.github.io",
  "description": "jingwei.link blog"
}

上面的 JSON 是我通过 curl -H "Accept: application/vnd.github.mercy-preview+json" https://api.github.com/search/repositories?q=chalvern 命令在结果中抽取出来的(参考 Search - GitHub Developer Guide),其实是 jingwei.link 这个博客内容的托管仓库相关的信息。

第一种用法:带标签(tag)的结构体(struct)

不废话,直接上代码:

// cat $GOPATH/src/jingwei.link/stuct/main.go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	a := `{"name":"chalvern.github.io","full_name":"chalvern/chalvern.github.io","private":false,"owner":{"login":"chalvern","html_url":"https://github.com/chalvern"},"html_url":"https://github.com/chalvern/chalvern.github.io","description":"jingwei.link blog"}`

	// OwnerS 会被嵌套在 JingweiS 中
	// 在每个字段后的 `json:xxx` 即 tag,与 json 中的 key 对应
	type OwnerS struct {
		Login   string `json:"login"`
		HTMLURL string `json:"html_url"`
	}

	//  JingweiS 中嵌套了 OwnerS 用来对应 a 变量 json 字符串中的嵌套的 json 内容
	type JingweiS struct {
		Name        string `json:"name"`
		FullName    string `json:"full_name"`
		Private     bool   `json:"private"`
		Owner       OwnerS `json:"owner"`
		HTMLURL     string `json:"html_url"`
		Description string `json:"description"`
	}

	var jingwei JingweiS
	err := json.Unmarshal([]byte(a), &jingwei)
	fmt.Printf("%#v \n %#v \n", err, jingwei)
  // 接下来可以通过 “.” 运算符获取期望的值了
	fmt.Printf("%#v\n", jingwei.Owner.Login)
}

把上面的代码复制到一个文本中,然后运行代码可以得到下面的输出:

# cd $GOPATH/src/jingwei.link/stuct/ && go run main.go
# 下面为输出的内容
<nil> 
 main.JingweiS{Name:"chalvern.github.io", FullName:"chalvern/chalvern.github.io", Private:false, Owner:main.OwnerS{Login:"chalvern", HTMLURL:"https://github.com/chalvern"}, HTMLURL:"https://github.com/chalvern/chalvern.github.io", Description:"jingwei.link blog"} 
"chalvern"

第二种用法:interface{}与断言的组合

不废话,还是直接上代码:

// cat $GOPATH/src/jingwei.link/interface/main.go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	a := `{"name":"chalvern.github.io","full_name":"chalvern/chalvern.github.io","private":false,"owner":{"login":"chalvern","html_url":"https://github.com/chalvern"},"html_url":"https://github.com/chalvern/chalvern.github.io","description":"jingwei.link blog"}`

	var jingweiI interface{}
	err := json.Unmarshal([]byte(a), &jingweiI)
	fmt.Printf("%#v \n %#v \n", err, jingweiI)
	// 获取某个 key 的值
	jingweiM, ok := jingweiI.(map[string]interface{})
	if !ok {
		fmt.Println("DO SOMETHING!")
		return
	}
	fmt.Printf("%#v\n", jingweiM["name"])
	//  获取嵌套的内容
	owner, ok := jingweiM["owner"].(map[string]interface{})
	if !ok {
		fmt.Println("DO SOMETHING!")
		return
	}
	fmt.Printf("%#v\n", owner["login"])
}

把上面的代码复制到一个文本中,然后运行代码可以得到下面的输出:

# cd $GOPATH/src/jingwei.link/interface/ && go run main.go
# 下面为输出的内容
<nil> 
 map[string]interface {}{"description":"jingwei.link blog", "full_name":"chalvern/chalvern.github.io", "html_url":"https://github.com/chalvern/chalvern.github.io", "name":"chalvern.github.io", "owner":map[string]interface {}{"html_url":"https://github.com/chalvern", "login":"chalvern"}, "private":false} 
"chalvern.github.io"
"chalvern"

两种用法的对比

通过上面的代码可以知道,通过 ① 带标签(tag)的结构体(struct)和 ② interface{}与断言的组合 都可以拿到 JSON 中的某个 Key 的值。前者逻辑上更为严密,也是我个人比较喜欢的一种模式(心里踏实),多用来处理服务之间接口调用相关的逻辑(实际上也是 gRPC 的用法);后者相对要灵活一些,尤其在 Key 非常多或者经常改变的情况下,被认为可扩展性强一些(在使用的过程中需要特别注意断言的用法,考虑各种边界条件,否则很容易出错 =。=)。

小结

懂得的东西越多,越发现有那么多的知识(包括常识与一些“熟视无睹”的道理)值得把玩。

可能是因为到了啰里啰嗦的年纪,越来越觉得细节很重要!细节很重要!很重要!要!

扣细节,并且乐在其中,不知达到了何种境界🤔。

参考