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

41次阅读

共计 4820 个字符,预计需要花费 13 分钟才能阅读完成。

引言

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。

参考资料

  • 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。

正文完
 0