前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你用 reflect 包解析 Go 的结构体 - Step 1: 参数类型检查

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

原创
作者头像
amc
修改2021-07-10 11:23:09
1.6K0
修改2021-07-10 11:23:09
举报
文章被收录于专栏:后台全栈之路后台全栈之路

Go 原生的 encoding/jsonUnmarshalMarshal 函数的入参为 interface{},并且能够支持任意的 struct 或 map 类型。这种函数模式,具体是如何实现的呢?本文便大略探究一下这种实现模式的基础:reflect 包。


基本概念

interface{}

初学 Go,很快就会接触到 Go 的一个特殊类型:interface。Interface 的含义是:实现指定 interface 体内定义的函数的所有类型。举个例子,我们有以下的接口定义:

代码语言:go
复制
type Dog interface{
    Woof()
}

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

代码语言:go
复制
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 互转。

首先我们定义一个函数:

代码语言:go
复制
func Marshal(v interface{}) ([]byte, error)

内部实现上,逻辑是先解析入参的字段信息,转成原生的 url.Values 类型,然后再调用 Encode 函数转为字节串输出即可,这样一来特殊字符的转义咱们就不用操心了。

代码语言:go
复制
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() 获得,这个类型包含了入参的所有与数据类型相关的信息:

代码语言:go
复制
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:

代码语言:go
复制
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,咱们的入参数检查可以这么写:

代码语言:go
复制
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。这一来,入参合法性检查的代码挺长了,咱们把合法性检查抽成一个专门的函数吧。因此上面的函数片段,我们改写成这样:

代码语言:go
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本概念
    • interface{}
      • reflect
      • 实验场景
        • 入参类型检查 —— reflect.Type
          • 入参值迭代 —— reflect.Value
          • 下一步
          • 参考资料
          • 其他文章推荐
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档