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

amc
English

引言

Go 原生的 encoding/jsonUnmarshalMarshal 函数的入参为 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.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 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)

这里,我们说 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

参考资料

其他文章推荐


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,原文发布于云+社区,也是本人的博客。欢迎转载,但请注明出处。

原文标题:《手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查》

发布日期:2021-06-28

原文链接:https://cloud.tencent.com/developer/article/1839823

阅读 461

后台 / 嵌入式全栈之路
曾经的嵌入式 / 后台开发一枚,现在开始走向架构。本专栏没有高深技术,只讲基础组件、工具,请放心食用

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发

833 声望
175 粉丝
0 条评论
你知道吗?

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发

833 声望
175 粉丝
宣传栏