在使用 Go 语言开发过程中,我们经常需要实现结构体到 JSON 字符串的序列化(Marshalling)或 JSON 字符串到结构体的反序列化(Unmarshalling)操作。Go 为我们提供了 encoding/json
库可以很方便的实现这一需求。
在本文中,我们将探索如何使用 Go 的反射机制自己来实现一个简易版的 encoding/json
库。这个过程不仅能帮助我们理解序列化和反序列化的基本原理,还能提供一种实用的反射使用方法,加深我们对反射的理解。
通过本文的学习,我们将实现一个能够将结构体转和 JSON 字符串互相转换的包。
encoding/json
我们先来回顾下在 Go 中如何使用 encoding/json
库实现结构体转和 JSON 字符串互转。
示例代码如下:
1 | package main |
示例程序中定义了一个 User
结构体,结构体包含三个字段,Name
、Age
和 Email
。
encoding/json
会根据结构体字段上的 JSON Tag
(标签)进行序列化和反序列化。序列化时,JSON Tag
会作为 JSON 字符串的 key
,字段值作为 JSON 字符串的 value
。反序列化时,JSON 字符串的 key
所对应的值会被映射到具有同样 JSON Tag
的结构体字段上。
Name
字段的 JSON Tag
是 name
,则对应的 JSON 字符串 key
为 name
;Age
字段的 JSON Tag
是 age
,则对应的 JSON 字符串 key
为 age
;Email
字段没有 JSON Tag
,则默认会使用 Email
作为对应的 JSON 字符串 key
。
执行示例代码,得到如下输出:
1 | $ go run main.go |
reflect 简介
reflect
是 Go 语言为我们提供的反射库,用于在运行时检查类型并操作对象。它是实现动态编程和元编程的基础,使程序能够在运行时获取类型信息并进行相应的操作。
有如下示例代码:
1 | package main |
执行示例代码,得到如下输出:
1 | $ go run main.go |
reflect
最常用的两个方法分别是 reflect.ValueOf
和 reflect.TypeOf
,它们分别返回 reflect.Value
和 reflect.Type
类型。这两个方法可以应用于任何类型对象(any
)。
reflect.Value
:表示一个 Go 值,它提供了一些方法,可以获取值的详细信息,也可以操作值,例如获取值的类型、设置值等。reflect.Type
:表示一个 Go 类型,它提供了一些方法,可以获取类型的详细信息,例如类型的名称(Name
)、种类(Kind
,基本类型、结构体、切片等)。
接下来对 reflect.Value
和 reflect.Type
类型的常用方法进行介绍,以如下实例化 User
结构体指针作为被操作对象:
1 | // 实例化 User 结构体指针 |
reflect.Value 常用方法
reflect.Value
提供了 Kind
方法可以获取对应的类型类别
:
1 | // 注意这里传递的是指针类型 |
这段示例代码将得到如下输出:
1 | ptr |
这里 Kind
方法返回的是 User
的底层类型 struct
,以及 ptr
类型,ptr
代表指针类型。
值得注意的是,如果传递给 reflect.ValueOf
的是指针类型(user
),需要使用 Elem
方法获取指针指向的值;如果传递给 reflect.ValueOf
的是值类型(*user
),则可以直接得到值。
使用指针类型的好处是可以使用 reflect.Value
提供的 Set<Type>
方法直接修改 user
字段的值,稍后讲解。
reflect.Value
同样提供了 Type
方法,可以得到 reflect.Type
:
1 | // 以下二者等价 |
这与 reflect.TypeOf
等价。
这段示例代码将得到如下输出:
1 | *main.User |
我们有多种方式可以获取结构体值字段:
1 | nameField := reflect.ValueOf(user).Elem().FieldByName("Name") |
FieldByName
方法可以通过字段名获取结构体字段。FieldByIndex
方法通过索引切片获取结构体字段。Field
方法通过索引获取结构体字段。
实际上 FieldByIndex
方法内部调用的也是 Field
方法。这里的索引是结构体字段按照顺序排序所在位置,即 Name
字段索引为 0,Age
字段索引为 1,Email
字段索引为 2。
我们可以使用 NumField
获取结构体字段总个数:
1 | numField := reflect.ValueOf(*user).NumField() |
拿到结构体字段对象后,可以根据其具体类型获取对应值:
1 | fmt.Println(nameField.String()) |
以上示例代码将得到如下输出:
1 | 3 |
因为我们传递给 reflect.ValueOf
函数的是 User
结构体指针,所以可以使用 reflect.Value
提供的 Set<Type>
方法设置结构体字段的值:
1 | nameField.SetString("jianghushinian") // 设置 Name 字段的值 |
现在打印 user
对象:
1 | fmt.Println(user) |
得到输出:
1 | &{jianghushinian 18 jianghushinian007@163.com} |
如果我们传递给 reflect.ValueOf
函数的不是 User
结构体指针,而是结构体对象:
1 | nameField := reflect.ValueOf(*user).FieldByName("Name") |
现在去设置字段值:
1 | nameField.SetString("jianghushinian") |
程序会直接 panic
:
1 | panic: reflect: reflect.Value.SetString using unaddressable value |
此外,我们还可以总结一个规律,使用指针时,就需要通过 Elem
方法获取指针指向的值,不使用指针就不需要调用 Elem
方法。
reflect.Type 常用方法
现在我们再来简单介绍下 reflect.Type
的几个常用方法。
reflect.Type
同样提供了如下几个方法,与 reflect.Value
对应:
1 | nameField, _ := reflect.TypeOf(user).Elem().FieldByName("Name") |
我们来输出下这几个对象的值:
1 | fmt.Printf("%+v\n", nameField) |
得到如下输出:
1 | {Name:Name PkgPath: Type:string Tag:json:"name" Offset:0 Index:[0] Anonymous:false} |
这里打印了结构体每个字段的信息。
Name
对应字段名。PkgPath
是包路径。Type
是结构体字段类型。Tag
即为字段标签。Offset
是字段偏移量。如果你不清楚什么是字段偏移量,可以参考我写的另一篇文章《Go 语言中的结构体内存对齐你了解吗?》。Index
是字段索引位置。Anonymous
表示是否为匿名字段。比如如下结构体:
1 | type User struct { |
这个结构体定义中,最后一个字段就是匿名字段。
现在我们想获取结构体字段 JSON Tag
,可以这样做:
1 | tag := nameField.Tag |
将得到如下输出:
1 | json:"name" |
reflect
基础语法就讲解到这里,更多使用方法需要我们在以后的的实践中去探索。
使用 reflect 实现 encoding/json
接下来就看看,如何使用 reflect
自己实现一个简易版本的 encoding/json
。
示例程序目录结构如下:
1 | $ tree |
encoding/json/encode.go
用于实现序列化功能。encoding/json/decode.go
用于实现反序列化功能。main.go
用来验证这个简易版的encoding/json
功能。
序列化
首先是实现序列化的代码:
1 | package json |
这段代码中没有新的 reflect
语法,我们都在前文中介绍了,这里捋一下代码逻辑。
所谓序列化操作,就是 Go 结构体转 JSON 字符串的操作。
这里函数名参考 encoding/json
同样被定义为 Marshal
。
首先我们拿到对象 v
的 reflect.Value
和 reflect.Type
,待后续使用。
接着使用 strings.Builder
构造了一个用来保存 JSON 字符串信息的对象 jsonBuilder
。
构造 JSON 字符串分三步走:
先写入 JSON 左花括号
{
内容到jsonBuilder
。根据结构体字段和值,构造 JSON 字符串的键值对
key/value
并写入jsonBuilder
。最后写入 JSON 右花括号
}
内容到jsonBuilder
。
函数最终返回 jsonBuilder.String()
即为 JSON 字符串。
这里面主要逻辑都在步骤 2 中。
首先会遍历结构体每个字段,并使用如下方式获取每个字段对应的 JSON Tag
:
1 | tag := fieldType.Tag.Get("json") |
当 JSON Tag
不存在,则默认使用结构体字段名作为 JSON 字符串的 key
,比如 User.Email
字段。
将 JSON key
和 :
写入 jsonBuilder
:
1 | jsonBuilder.WriteString(`"` + tag + `":`) |
然后根据结构体字段类型转换成对应的 JSON 数据类型,写入 jsonBuilder
:
1 | switch fieldVal.Kind() { |
每次循环末尾,判断是否为结构体最后一个字段,如果不是,则写入分隔符 ,
:
1 | if i < val.NumField()-1 { |
至此,序列化代码逻辑大功告成。
我们可以使用如下示例代码进行测试:
1 | import simplejson "github.com/jianghushinian/blog-go-example/struct/encoding-json/encoding/json" |
执行示例代码,得到如下输出:
1 | $ go run main.go |
没有任何问题,与原生的 encoding/json
中的 Marshal
方法表现一致。
反序列化
接下来是实现反序列化的代码:
1 | package json |
这段代码中同样没有新的 reflect
语法。
所谓反序列化操作,就是 JSON 字符串转 Go 结构体的操作。
这里函数名参考 encoding/json
同样被定义为 Unmarshal
,并且函数签名也保持一致。
反序列化操作首先使用 parseJSON
函数解析传递进来的 JSON 数据,得到 parsedData
。
parsedData
类型为 map[string]string
,map
的 key
为 JSON 字符串中的 key
,map
的 value
即为 JSON 字符串中的 value
。
接下来核心逻辑是遍历结构体每个字段,并获取字段对应的 JSON Tag
:
1 | tag := fieldType.Tag.Get("json") |
当 JSON Tag
不存在,则默认使用结构体字段名作为 JSON 字符串的 key
,比如 User.Email
字段。
然后根据 JSON Tag
从解析后的 parsedData
数据中获取 key/value
:
1 | if value, ok := parsedData[tag]; ok { |
这里根据结构体字段的类型,将 parsedData
中对应的字符串 value
转换成对应类型。并使用 reflect.Value
提供的 SetString
和 SetInt
方法设置字段的值。
现在,我们唯一没有讲解的逻辑就只剩下 parseJSON
函数了。
parseJSON
函数定义如下:
1 | // 简易版 JSON 解析器,仅支持 string/int 且不考虑嵌套 |
parseJSON
实现了一个简易版本的 JSON 字符串解析器,能够将 JSON 字符串的 key/value
解析出来,并保存到 map[string]string
中。
我们可以使用如下示例代码进行测试 Unmarshal
代码逻辑是否正确:
1 | import simplejson "github.com/jianghushinian/blog-go-example/struct/encoding-json/encoding/json" |
执行示例代码,得到如下输出:
1 | $ go run main.go |
没有任何问题,与原生的 encoding/json
中的 Unmarshal
方法表现一致。
总结
reflect
是 Go 语言为我们提供的反射库,用于在运行时检查类型并操作对象。
reflect
最常用的两个方法分别是 reflect.ValueOf
和 reflect.TypeOf
,调用这两个方法分别可以得到 reflect.Value
和 reflect.Type
类型。
有了这两个类型及其方法,我们可以获取任意一个 Go 对象的类型信息、值的详细信息和操作值,可见反射之强大。
本文使用 reflect
反射包实现了一个简易版本的 encoding/json
。
虽然是简易版本,很多 case
和异常都没有考虑,但这足够我们学习 encoding/json
原理了,并且这也是一个很好的 reflect
实践应用。
不过话虽如此,对于何时使用反射,我的观点是:反射固然强大,有了它我们的代码足够灵活,但是过度使用反射会让代码变得复杂且混乱。所以非必要,尽量不要使用反射。
本文示例源码我都放在了 GitHub 中,欢迎点击查看。
希望此文能对你有所启发。
延伸阅读
- reflect@go1.22.5 Documentation:https://pkg.go.dev/reflect@go1.22.5
- encoding/json@go1.22.5 Documentation:https://pkg.go.dev/encoding/json@go1.22.5
- 本文 GitHub 示例代码:https://github.com/jianghushinian/blog-go-example/tree/main/struct/encoding-json
联系我
- 公众号:Go编程世界
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客:https://jianghushinian.cn