手把手教你用 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://cloud.tencent.com/developer/article/1839823。

转载自:https://segmentfault.com/a/1190000040325629