手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查
引言
Go 原生的 encoding/json
的 Unmarshal
和 Marshal
函数的入参为 interface{}
,并且能够支持任意的 struct 或 map 类型。这种函数模式,具体是如何实现的呢?本文便大略探究一下这种实现模式的基础:reflect
包。
基本概念
interface{}
初学 Go,很快就会接触到 Go 的一个特殊类型:interface
。Interface 的含义是:实现指定 interface 体内定义的函数的所有类型。举个例子,我们有以下的接口定义:
type Dog interface{
Woof()
}
那么只要是实现了 Woof()
函数(汪汪叫),都可以认为是实现了 Dog
接口的类型。注意,是所有类型,不局限于复杂类型或者是基本类型。比如说我们用 int
重新定义一个类型,也是可以的:
type int FakeDog
func (d FakeDog) Woof() {
// do hothing
}
好,接下来,我们又会见到一个常见的写法:interface{}
,interface 单词紧跟着一个未包含任何内容的花括号。我们要知道,Go 支持匿名类型,因此这依然是一种接口类型,只是这个接口没有规定任何需要实现的函数。
那么从语义上我们可以知道,任意类型都符合这个接口的定义。反过来说,interface{}
就可以用来表示任意类型。这就是 json marshaling 和 unmarshaling 的入参。
reflect
OK,虽然有了 interface{}
用于表示 “任意类型”,但是我们最终总得解析这个 “任意类型” 参数吧?Go 提供了 reflect
包,用来解析。这就是中文资料中常提的 “反射机制”。反射可以做很多事情,本文中我们主要涉及解析结构体的部分。
以下,我们设定一个实验 / 应用场景,来一步步介绍 reflect 的用法和注意事项。
实验场景
各种主流的序列化 / 反序列化协议如 json、yaml、xml、pb 什么的都有权威和官方的库了;不过在 URL query 场景下,相对还不特别完善。我们就拿这个场景来玩一下吧 —— URL query 和 struct 互转。
首先我们定义一个函数:
func Marshal(v interface{}) ([]byte, error)
内部实现上,逻辑是先解析入参的字段信息,转成原生的 url.Values
类型,然后再调用 Encode
函数转为字节串输出即可,这样一来特殊字符的转义咱们就不用操心了。
func Marshal(v interface{}) ([]byte, error) {
kv, err := marshalToValues(v)
if err != nil {
return nil, err
}
s := kv.Encode()
return []byte(s), nil
}
func marshalToValues(in interface{}) (kv url.Values, err error) {
// ......
}
入参类型检查 —— reflect.Type
首先我们看到,入参是一个 interface{}
,也就是 “任意类型”。表面上是任意类型,但实际上并不是所有数据类型都是支持转换的呀,因此这里我们就需要对入参类型进行检查。
这里我们就遇到了第一个需要认识的数据类型:reflect.Type
。reflect.Type
通过 reflect.TypeOf(v)
或者是 reflect.ValueOf(v).Type()
获得,这个类型包含了入参的所有与数据类型相关的信息:
func marshalToValues(in interface{}) (kv url.Values, err error) {
if in == nil {
return nil, errors.New("no data provided")
}
v := reflect.ValueOf(in)
t := v.Type()
// ......
}
按照需求,我们允许的入参是结构体或者是结构体指针。这里用到的是 reflect.Kind
类型。
Kind 和 type 有什么区别呢?首先我们知道,Go 是强类型语言(超强!),使用 type newType oldType
这样的语句定义出来的两个类型,虽然可以通过显式的类型转换,但是直接进行赋值、运算、比较等等操作时,是无法通过的,甚至可能造成 panic:
package main
import "fmt"
func main() {
type str string
s1 := str("I am a str")
s2 := "I am a string"
fmt.Println(s1 == s2)
}
// go run 无法通过,编译信息为:
// ./main.go:9:17: invalid operation: s1 == s2 (mismatched types str and string)
这里,我们说 str
和 string
的 type
是不同的。但是我们可以说,str
和 string
的 kind
是相同的,为什么呢?Godoc 对 Kind 的说明为:
- A Kind represents the specific kind of type that a Type represents. The zero Kind is not a valid kind.
注意 “kind of type”,kind 是对 type 的进一步分类,Kind 涵盖了所有的 Go 数据类型,通过 Kind,我们可以知道一个变量的底层类型是什么。Kind 是一个枚举值,下面是完整的列表:
reflect.Invaid
: 表示不是一个合法的类型值reflect.Bool
: 布尔值,任意type xxx bool
甚至是进一步串联下去的定义,都是这个 kind。以下类似。reflect.Int
,reflect.Int64
,reflect.Int32
,reflect.Int16
,reflect.Int8
: 各种有符号整型类型。严格而言这些类型的 kind 都不同,不过往往可以一并处理。原因后面会提及。reflect.Uint
,reflect.Uint64
,reflect.Uint32
,reflect.Uint16
,reflect.Uint8
: 各种无符号整型类型。reflect.Uintptr
:uintptr
类型reflect.Float32
,reflect.Float64
: 浮点类型reflect.Complex32
,reflect.Complex64
: 复数类型reflect.Array
: 数组类型。注意与切片的差异reflect.Chan
: Go channel 类型reflect.Func
: 函数reflect.Interface
:interface
类型。自然地,interface{}
也属于此种类型reflect.Map
:map
类型reflect.Ptr
: 指针类型reflect.Slice
: 切片类型。注意与数组的差异reflect.String
:string
类型reflect.Struct
: 结构体类型reflect.UnsafePointer
:unsafe.Pointer
类型
看着好像有点眼花缭乱?没关系,我们这里先作最简单的检查——现阶段我们检查整个函数的入参,只允许结构体或者是指针类型,其他的一概不允许。OK,咱们的入参数检查可以这么写:
func marshalToValues(in interface{}) (kv url.Values, err error) {
// ......
v := reflect.ValueOf(in)
t := v.Type()
if k := t.Kind(); k == reflect.Struct || k == reflect.Ptr {
// OK
} else {
return nil, fmt.Errorf("invalid type of input: %v", t)
}
// ......
}
入参检查还没完。如果入参是一个 struct,那么很好,我们可以摩拳擦掌了。但如果入参是指针,要知道,指针可能是任何数据类型的指针呀,所以我们还需要检查指针的类型。
如果入参是一个指针,我们可以跳用 reflect.Type
的 Elem()
函数,获得它作为一个指针,指向的数据类型。然后我们再对这个类型做检查即可了。
这次,我们只允许指向一个结构体,同时,这个结构体的值不能为 nil。这一来,入参合法性检查的代码挺长了,咱们把合法性检查抽成一个专门的函数吧。因此上面的函数片段,我们改写成这样:
func marshalToValues(in interface{}) (kv url.Values, err error) {
v, err := validateMarshalParam(in)
if err != nil {
return nil, err
}
// ......
}
func validateMarshalParam(in interface{}) (v reflect.Value, err error) {
if in == nil {
err = errors.New("no data provided")
return
}
v = reflect.ValueOf(in)
t := v.Type()
if k := t.Kind(); k == reflect.Struct {
// struct 类型,那敢情好,直接返回
return v, nil
} else if k == reflect.Ptr {
if v.IsNil() {
// 指针类型,值为空,那就算是 struct 类型,也无法解析
err = errors.New("nil pointer of a struct is not supported")
return
}
// 检查指针指向的类型是不是 struct
t = t.Elem()
if t.Kind() != reflect.Struct {
err = fmt.Errorf("invalid type of input: %v", t)
return
}
return v.Elem(), nil
}
err = fmt.Errorf("invalid type of input: %v", t)
return
}
入参值迭代 —— reflect.Value
从上一个函数中,我们遇到了需要认识的第二个数据类型:reflect.Value
。reflect.Value
通过 reflect.ValueOf(v)
获得,这个类型包含了目标参数的所有信息,其中也包含了这个变量所对应的 reflect.Type
。在入参检查阶段,我们只涉及了它的三个函数:
Type()
: 获得reflect.Type
值Elem()
: 当变量为指针类型时,则获得其指针值所对应的reflect.Value
值IsNil()
: 当变量为指针类型时,可以判断其值是否为空。其实也可以跳过IsNil
的逻辑继续往下走,那么在t = t.Elem()
后面,会拿到reflect.Invalid
值。
下一步
本文入了个门,检查了一下 interface{}
类型的入参。下一步我们就需要探索 reflect.Value
格式的结构体内部成员了,敬请期待。此外,本文的代码也可以在 Github 上找到,本阶段的代码对应 Commit 915e331。
参考资料
- Checking reflect.Kind on interface{} return invalid result
- Go reflection 三定律与最佳实践 - 3. reflect.Value 数据结构
其他文章推荐
- Go 语言设计与实现 - 4.3 反射
- 还在用
map[string]interface{}
处理 JSON?告诉你一个更高效的方法——jsonvalue - Go 语言原生的 json 包有什么问题?如何更好地处理 JSON 数据?
- 手把手教你用 reflect 包解析 Go 的结构体 - Step 2: 结构体成员遍历
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原作者: amc,原文发布于云+社区,也是本人的博客。欢迎转载,但请注明出处。
原文标题:《手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查》
发布日期:2021-06-28
转载自:https://segmentfault.com/a/1190000040325629