引言
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。