云原生系列Go语言篇-标准库Part 2

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
注册配置 MSE Nacos/ZooKeeper,118元/月
函数计算FC,每月15万CU 3个月
简介: REST API将JSON奉为服务之通信的标准方式,Go 的标准库内置对Go 数据类型与 JSON 之间进行转换的支持。marshaling一词表示从 Go 数据类型转为另一种编码,而unmarshaling表示转换为 Go 数据类型。

encoding/json

REST API将JSON奉为服务之通信的标准方式,Go 的标准库内置对Go 数据类型与 JSON 之间进行转换的支持。marshaling一词表示从 Go 数据类型转为另一种编码,而unmarshaling表示转换为 Go 数据类型。

使用结构体标签添加元数据

假设我们正在构建一个订单管理系统,并且需要读取和写入以下 JSON:

{
    "id":"12345",
    "date_ordered":"2020-05-01T13:01:02Z",
    "customer_id":"3",
    "items":[{"id":"xyz123","name":"Thing 1"},{"id":"abc789","name":"Thing 2"}]
}

我们定义映射该数据的类型:

type Order struct {
    ID            string        `json:"id"`
    DateOrdered time.Time `json:"date_ordered"`
    CustomerID    string        `json:"customer_id"`
    Items         []Item        `json:"items"`
}
type Item struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

我们使用结构体标签来指定处理JSON数据的规则,也即结构体内字段后面的字符串。尽管结构体标签是用反引号标记的字符串,但它们要放在同一行。结构体标签由一个或多个标签/值对组成,写作tagName:"tagValue",并用空格分隔。由于它们只是字符串,编译器无法验证其格式是否正确,但go vet可以进行验证。此外,请注意这些字段都是导出的。与其他包一样,encoding/json包中的代码无法访问另一个包中结构体的未导出字段。

对于JSON的处理,我们使用标签名json来指定与结构体字段关联的JSON字段的名称。如果没有提供json标签,那么默认行为是假定JSON 对象字段的名称与 Go 结构体字段的名称相匹配。尽管有这种默认行为,即使字段名称相同,最好也使用结构体标签显式指定字段的名称。

注:在将JSON反序列化到没有json标签的结构体字段时,名称匹配是不区分大小写的。在没有json标签的结构体字段序列化为JSON 时,JSON 字段的首字母始终是大写的,因为该字段是导出的。

如果在序列化或反序列化时需忽略某个字段,对该字段的名称使用破折号(-)。如果该字段在为空时应在输出中省云,可以在名称后添加,omitempty

警告:“空”定义与零值不完全对齐,可能读者也猜到了。结构体的零值不作为空,但是零长切片或字典则视为空。

结构体标签允许我们使用元数据来控制程序的行为。其他语言,尤其是 Java,鼓励开发人员在各种程序元素上放置注解,来描述应该如何处理它们,而并不明确指定进行处理的方式。虽然声明式编程可以使程序更加简洁,但元数据的自动处理会让程序的行为变得难以理解。任何使用过带有注解的大型 Java 项目的人都会在出现问题时陷入恐慌,因为他们不知道哪段代码正在处理特定的注解以及它做出了什么变化。Go 更偏向于显式的代码而不是短小的代码。结构体标签永远不会自动运行;它们在将结构体实例传递给函数时进行处理。

序列化和反序列化

encoding/json包中的Unmarshal函数用于将字节切片转换为结构体。如果我们有一个名为data的字符串,以下是将data转换为Order类型结构体的代码:

var o Order
err := json.Unmarshal([]byte(data), &o)
if err != nil {
    return err
}

json.Unmarshal函数将数据填充到一个入参中,就像io.Reader接口的实现一样。这样做有两个原因。首先,像io.Reader的实现一样,这样可对相同的结构体进行高效的重用,从而控制内存使用。其次,没有其它实现的方式。因为Go长时间没有泛型,所以无法指定应该实例化哪种类型来存储正在读取的字节。即使Go添加了泛型,内存使用的优势也依旧存在。

我们使用encoding/json包中的Marshal函数将Order实例以 JSON 的形式写回,并存储在一个字节切片中:

out, err := json.Marshal(o)

这带来了一个问题:我们是如何处理结构标签的?你可能还想知道为什么 json.Marshaljson.Unmarshal能够读取和写入任意类型的结构体。毕竟,我们编写的其他方法都只能处理在程序编译时已知的类型(甚至类型开关中列出的类型也是预先枚举的)。这两个问题的答案都是反射。可以在恶龙三剑客:反射、Unsafe 和 Cgo中了解更多关于反射的内容。

JSON、Reader和Writer

json.Marshaljson.Unmarshal函数处理的是字节切片。刚刚也看到了,Go 中的大部分数据源和数据宿都实现了io.Readerio.Writer接口。虽然可以使用ioutil.ReadAllio.Reader的全部内容复制到字节切片中,以供json.Unmarshal 读取,但这样做效率低下。同样,我们可以使用json.Marshal将数据写入内存中的字节切片缓冲区,然后将其写入网络或磁盘,但如果我们可以直接写入io.Writer,会更好。

encoding/json包有两种类型供我们处理这些场景。json.Decoderjson.Encoder类型分别从实现了io.Readerio.Writer接口的任意内容进行读取和写入。让我们快速看一下它们是如何工作的。

我们从一个实现简单结构体的toFile中的数据开始:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
toFile := Person {
    Name: "Fred",
    Age:  40,
}

os.File类型同时实现了io.Readerio.Writer接口,我们可以使用它来演示json.Decoderjson.Encoder。首先,我们将toFile写入一个临时文件,将临时文件传递给json.NewEncoder,它返回该临时文件的json.Encoder。然后,我们将json.Encoder传递给Encode方法:

tmpFile, err := ioutil.TempFile(os.TempDir(), "sample-")
if err != nil {
    panic(err)
}
defer os.Remove(tmpFile.Name())
err = json.NewEncoder(tmpFile).Encode(toFile)
if err != nil {
    panic(err)
}
err = tmpFile.Close()
if err != nil {
    panic(err)
}

写入toFile后,我们可以通过将临时文件的指针传递给json.NewDecoder,并在返回的json.Decoder上调用Decode方法,将其读取为 JSON,并使用类型为Person的变量来接收:

tmpFile2, err := os.Open(tmpFile.Name())
if err != nil {
    panic(err)
}
var fromFile Person
err = json.NewDecoder(tmpFile2).Decode(&fromFile)
if err != nil {
    panic(err)
}
err = tmpFile2.Close()
if err != nil {
    panic(err)
}
fmt.Printf("%+v\n", fromFile)

完整示例请见Playground

JSON数据流编解码

在需要一次读取或写入多个JSON结构体时该怎么办做呢?可以使用我们的老朋友json.Decoderjson.Encoder处理这些情况。

假设有以下数据:

{"name": "Fred", "age": 40}
{"name": "Mary", "age": 21}
{"name": "Pat", "age": 30}

对于我们的示例,假设数据存储在一个名为data的字符串中,但它也可以是文件,甚至是传入的HTTP请求(我们稍后会了解HTTP服务端的原理)。

我们将该数据存在到变量t中,每次一个JSON 对象。

和之前一样,我们使用数据源初始化json.Decoder,但这次我们使用json.DecoderMore方法作为for循环条件。这样可以逐个读取数据,每次一个JSON 对象:

var t struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
dec := json.NewDecoder(strings.NewReader(data))
for dec.More() {
    err := dec.Decode(&t)
    if err != nil {
        panic(err)
    }
    // process t
}

使用json.Encoder写多个值的方式与写单个值的方式相同。本例中,我们写入bytes.Buffer,但任意实现io.Writer接口的类型都可以:

var b bytes.Buffer
enc := json.NewEncoder(&b)
for _, input := range allInputs {
    t := process(input)
    err = enc.Encode(t)
    if err != nil {
        panic(err)
    }
}
out := b.String()

可在Playground中运行本示例。

我们示例数据流中有多个没有封装到数组中的JSON 对象,但读者也可以使用json.Decoder从数组中读取单个对象,而无需一次性将整个数组加载到内存中。这可以大幅提升性能并减少内存使用。在Go文档中有一个示例。

自定义JSON 解析

虽然默认功能通常已足够使用,但有时需要进行重载。尽管time.Time默认支持 RFC 339 格式的 JSON 字段,但可能需要处理其他时间格式。我们可以通过创建一个实现json.Marshalerjson.Unmarshaler两个接口的新类型来进行处理:

type RFC822ZTime struct {
    time.Time
}
func (rt RFC822ZTime) MarshalJSON() ([]byte, error) {
    out := rt.Time.Format(time.RFC822Z)
    return []byte(`"` + out + `"`), nil
}
func (rt *RFC822ZTime) UnmarshalJSON(b []byte) error {
    if string(b) == "null" {
        return nil
    }
    t, err := time.Parse(`"`+time.RFC822Z+`"`, string(b))
    if err != nil {
        return err
    }
    *rt = RFC822ZTime{t}
    return nil
}

我们将一个time.Time实例内嵌到名为RFC822ZTime的新结构体中,这样仍可以访问time.Time的其他方法。就像我们在指针接收器和值接收器中讨论的那样,读取时间值的方法对值接收器声明,而修改时间值的方法对指针接收器声明。

然后,我们更改了DateOrdered字段的类型,可使用 RFC 822 格式的时间进行操作:

type Order struct {
    ID          string      `json:"id"`
    DateOrdered RFC822ZTime `json:"date_ordered"`
    CustomerID  string      `json:"customer_id"`
    Items       []Item      `json:"items"`
}

可在Playground中运行这段代码。

这种方法存在一个缺点:JSON的日期格式决定了数据结构中字段的类型。这是encoding/json方案本身的不足。可以让Order实现json.Marshalerjson.Unmarshaler,但那会要求你编写代码处理所有字段,包括那些不需要自定义支持的字段。结构体标签格式没有提供指定函数来解决具体字段的方式。这样我们就得为该字段创建一个自定义类型了。

另一种方式在Ukiah Smith的博客文章中进行了描述。我们可以只重新定义不符合默认序列化行为的字段,利用到结构体内嵌所做的 JSON 序列化和反序列化(我们在使用内嵌实现组合中进行了讲解)。如果嵌套结构体的字段名与外层结构体中的相重复,在序列化和反序列化时就会忽略该字段。

本例中,Order中的字段如下:

type Order struct {
    ID          string    `json:"id"`
    Items       []Item    `json:"items"`
    DateOrdered time.Time `json:"date_ordered"`
    CustomerID  string    `json:"customer_id"`
}

MarshalJSON方法如下:

func (o Order) MarshalJSON() ([]byte, error) {
    type Dup Order
    tmp := struct {
        DateOrdered string `json:"date_ordered"`
        Dup
    }{
        Dup: (Dup)(o),
    }
    tmp.DateOrdered = o.DateOrdered.Format(time.RFC822Z)
    b, err := json.Marshal(tmp)
    return b, err
}

OrderMarshalJSON方法中,我们定义了底层类型为OrderDup类型。创建Dup的原因是基于其它类型的类型具有和底层类型相同的字段,但方法却不同。如果没有Dup,在调用json.Marshal时就会进入到对MarshalJSON的无限调用循环,最终导致栈溢出。

我们定义了一个包含DateOrdered字段并内嵌Dup的匿名结构体。然后将Order实例赋给tmp中的内嵌字段,将tmp中的DateOrdered字段赋值为时间格式RFC822Z,对tmp调用json.Marshal。这会生成所需的JSON输出。

UnmarshalJSON中的逻辑类似:

func (o *Order) UnmarshalJSON(b []byte) error {
    type Dup Order
    tmp := struct {
        DateOrdered string `json:"date_ordered"`
        *Dup
    }{
        Dup: (*Dup)(o),
    }
    err := json.Unmarshal(b, &tmp)
    if err != nil {
        return err
    }
    o.DateOrdered, err = time.Parse(time.RFC822Z, tmp.DateOrdered)
    if err != nil {
        return err
    }
    return nil
}

UnmarshalJSON中,json.Unmarshal调用o中字段(DateOrdered除外),因为它嵌套在tmp之中。解封后通过使用time.Parse处理tmp中的DateOrdered字段来反序列化o中的DateOrdered

可在The Go Playground 中运行这段代码。

虽然这样可以让Order中的一个字段不必绑定JSON格式,但OrderMarshalJSONUnmarshalJSON方法就与JSON中时间字段的格式发生了耦合。我们无法复用Order去支持其它时间格式的JSON。

为限制考虑JSON处理方式的代码量,定义两个不同的结构体。一个用于和JSON之间的转换,另一个用于数据数据。将JSON读入适配JSON的类型,然后拷贝至另一个类型。在写入JSON时,执行相反操作。这确实会导致一定的重复,但这会保持业务逻辑不依赖于连接协议。

可以将map[string]any传递给json.Marshaljson.Unmarshal来在JSON和Go之间进行转换,但把它用于代码的解释阶段,在清楚如何进行处理后替换为具体的类型。Go使用类型是有原因的,它表明了预期的数据及其类型。

虽然JSON是标准库中最常用的的编码器,Go还内置了其它编码器,如XML和Base64。如果你需要编码的数据格式无法在标准库或第三方库中找到支持,可以自行编写。我们会在使用反射编写数据序列化工具中学习到如何实现自己的编码器。

警告:标准库中内置了encoding/gob,这是针对Go的二进制表现,有点类似于Java的序列化。正如Java的序列化是Enterprise Java Beans和Java RMI的连接协议一样,gob协议用于net/rpc包中实现的Go RPC(远程过程调用)的连接格式。不要使用encoding/gobnet/rpc。如果你希望通过Go做远程方法调用,使用GRPC等标准协议,这样就限于某一种语言。不管你有多爱Go语言,如果希望服务有价值的话,就允许开发者使用其它语言调用它。

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

相关文章
|
1天前
|
程序员 Go PHP
为什么大部分的 PHP 程序员转不了 Go 语言?
【9月更文挑战第8天】大部分 PHP 程序员难以转向 Go 语言,主要因为:一、编程习惯与思维方式差异,如语法风格和编程范式;二、学习成本高,需掌握新知识体系且面临项目压力;三、职业发展考量,现有技能价值及市场需求不确定性。学习新语言虽有挑战,但对拓宽职业道路至关重要。
22 10
|
1天前
|
算法 程序员 Go
PHP 程序员学会了 Go 语言就能唬住面试官吗?
【9月更文挑战第8天】学会Go语言可提升PHP程序员的面试印象,但不足以 solely “唬住” 面试官。学习新语言能展现学习能力、拓宽技术视野,并增加就业机会。然而,实际项目经验、深入理解语言特性和综合能力更为关键。全面展示这些方面才能真正提升面试成功率。
20 10
|
1天前
|
编译器 Go
go语言学习记录(关于一些奇怪的疑问)有别于其他编程语言
本文探讨了Go语言中的常量概念,特别是特殊常量iota的使用方法及其自动递增特性。同时,文中还提到了在声明常量时,后续常量可沿用前一个值的特点,以及在遍历map时可能遇到的非顺序打印问题。
|
5天前
|
安全 大数据 Go
深入探索Go语言并发编程:Goroutines与Channels的实战应用
在当今高性能、高并发的应用需求下,Go语言以其独特的并发模型——Goroutines和Channels,成为了众多开发者眼中的璀璨明星。本文不仅阐述了Goroutines作为轻量级线程的优势,还深入剖析了Channels作为Goroutines间通信的桥梁,如何优雅地解决并发编程中的复杂问题。通过实战案例,我们将展示如何利用这些特性构建高效、可扩展的并发系统,同时探讨并发编程中常见的陷阱与最佳实践,为读者打开Go语言并发编程的广阔视野。
|
3天前
|
存储 Shell Go
Go语言结构体和元组全面解析
Go语言结构体和元组全面解析
|
8天前
|
Go
golang语言之go常用命令
这篇文章列出了常用的Go语言命令,如`go run`、`go install`、`go build`、`go help`、`go get`、`go mod`、`go test`、`go tool`、`go vet`、`go fmt`、`go doc`、`go version`和`go env`,以及它们的基本用法和功能。
20 6
|
8天前
|
存储 Go
Golang语言基于go module方式管理包(package)
这篇文章详细介绍了Golang语言中基于go module方式管理包(package)的方法,包括Go Modules的发展历史、go module的介绍、常用命令和操作步骤,并通过代码示例展示了如何初始化项目、引入第三方包、组织代码结构以及运行测试。
16 3
|
10天前
|
缓存 安全 Java
如何利用Go语言提升微服务架构的性能
在当今的软件开发中,微服务架构逐渐成为主流选择,它通过将应用程序拆分为多个小服务来提升灵活性和可维护性。然而,如何确保这些微服务高效且稳定地运行是一个关键问题。Go语言,以其高效的并发处理能力和简洁的语法,成为解决这一问题的理想工具。本文将探讨如何通过Go语言优化微服务架构的性能,包括高效的并发编程、内存管理技巧以及如何利用Go生态系统中的工具来提升服务的响应速度和资源利用率。
|
10天前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
11天前
|
Java 数据库连接 数据库
携手前行:在Java世界中深入挖掘Hibernate与JPA的协同效应
【8月更文挑战第31天】Java持久化API(JPA)是一种Java规范,为数据库数据持久化提供对象关系映射(ORM)方法。JPA定义了实体类与数据库表的映射及数据查询和事务控制方式,确保不同实现间的兼容性。Hibernate是JPA规范的一种实现,提供了二级缓存、延迟加载等丰富特性,提升应用性能和可维护性。通过结合JPA和Hibernate,开发者能编写符合规范且具有高度可移植性的代码,并利用Hibernate的额外功能优化数据持久化操作。
27 0