手把手教你用|手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查

引言 Go 原生的 encoding/jsonUnmarshalMarshal 函数的入参为 interface{},并且能够支持任意的 struct 或 map 类型。这种函数模式,具体是如何实现的呢?本文便大略探究一下这种实现模式的基础:reflect 包。
基本概念 interface{}
初学 Go,很快就会接触到 Go 的一个特殊类型:interface。Interface 的含义是:实现指定 interface 体内定义的函数的所有类型。举个例子,我们有以下的接口定义:

type Dog interface{ Woof() }

那么只要是实现了 Woof() 函数(汪汪叫),都可以认为是实现了 Dog 接口的类型。注意,是所有类型,不局限于复杂类型或者是基本类型。比如说我们用 int 重新定义一个类型,也是可以的:
type int FakeDogfunc (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)

【手把手教你用|手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查】内部实现上,逻辑是先解析入参的字段信息,转成原生的 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.Typereflect.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 mainimport "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)

这里,我们说 strstringtype 是不同的。但是我们可以说,strstringkind 是相同的,为什么呢?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.TypeElem() 函数,获得它作为一个指针,指向的数据类型。然后我们再对这个类型做检查即可了。
这次,我们只允许指向一个结构体,同时,这个结构体的值不能为 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.Valuereflect.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://cloud.tencent.com/developer/article/1839823。
手把手教你用|手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查
文章图片

    推荐阅读