1

手把手教你用 reflect 包解析 Go 的结构体 - Step 3: 复杂类型检查

 2 years ago
source link: https://segmentfault.com/a/1190000040707732
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一篇文章我们完成了对结构体中基本数据类型的解析。这一篇文章,则是真正令人头疼的、在前两篇文章未处理的几个主题了:

  1. 结构体中嵌套结构体
  2. Go map

结构体中的匿名成员

我们回来看一下上一篇文章中的 marshalToValues 函数,其中有一行 “ft.Anonymous”:

func marshalToValues(in interface{}) (kv url.Values, err error) {
    // ......

    // 迭代每一个字段
    for i := 0; i < numField; i++ {
        fv := v.Field(i) // field value
        ft := t.Field(i) // field type

        if ft.Anonymous {
            // TODO: 后文再处理
            continue
        }
        
        // ......
    }

    return kv, nil
}

前文提过,这表示当前的字段是一个匿名字段。在 Go 中,匿名成员经常用于实现接近于继承的功能,比如:

type Dog struct{
    Name string
}

func (d *Dog) Woof() {
    // ......
}

type Husky struct{
    Dog
}

这样一来,类型 Husky 就 “继承” 了 Dog 类型的 Name 字段,以及 Woof() 函数。

但是需要注意的是,在 Go 中,这不是真正意义上的继承。我们在通过 reflect 解析 Husky 的结构时会发现,它包含了一个 Dog 类型结构体,而这个结构体在代码分支中,就会进入到前文的 if ft.Anonymous {} 分支中。

第二个需要注意的点是:在 Go 中,不仅仅是 struct 能够作为匿名成员,实际上任意类型都可以匿名。因此在代码中需要区分这种情况。

OK,知道了上述注意点之后,我们就可以来处理匿名结构体的情况啦。如果说匿名结构体的主要目的是为了继承的效果,那么我们对待匿名结构体中的成员的态度,就是当作对待结构体本身普通成员的态度一样。把我们已经实现了的 marshalToValues 的逻辑稍微调整一下,将迭代逻辑单独抽出来,方便递归就行——注意下文 readFieldToKV 函数的第一个条件判断代码块:

func marshalToValues(in interface{}) (kv url.Values, err error) {
    // ......

    // 迭代每一个字段
    for i := 0; i < numField; i++ {
        fv := v.Field(i) // field value
        ft := t.Field(i) // field type

        readFieldToKV(&fv, &ft, kv) // 主要逻辑抽出到函数中进行处理
    }

    return kv, nil
}

func readFieldToKV(fv *reflect.Value, ft *reflect.StructField, kv url.Values) {
    if ft.Anonymous {
        numField := fv.NumField()
        for i := 0; i < numField; i++ {
            ffv := fv.Field(i)
            fft := ft.Type.Field(i)

            readFieldToKV(&ffv, &fft, kv)
        }
        return
    }
    if !fv.CanInterface() {
        return
    }
    
    // ...... 原来的 for 循环中的主逻辑
}

结构体中的切片和数组

上一小节我们对 marshalToValues 的逻辑进行了调整,将 readFieldToKV 函数抽了出来。这个函数首先判断 if ft.Anonymous,也就是是否匿名;然后再判断 if !fv.CanInterface(),也就是是否可以导出。

再往下走,我们处理的是结构体中的每一个成员。上一篇文章中我们已经处理了所有的简单数据类型,但是还有不少承载有效数据的变量类型我们还没有处理。这一小节,我们来看看切片和数组要如何做。

首先在本文中我们规定,对于数组,只支持成员为基本类型(bool,数字、字符串、布尔值)的数组,而不支持所谓 “任意类型”(也就是 interface{})和结构体(struct)的数组。

究其原因,是因为后我们我们准备使用点分隔符来区分数组内的数组,也就是说,采用诸如 msg.data 来表示 msg 结构体中的 data 成员。而 URL query 是采用同一个 key 重复出现多次来实现数组类型的,那如果重复出现了 msg.data,那我们应该解释为 msg[n].data 呢,还是 msg.data[n] 呢?

为了实现这一段代码,我们修改前文的 readFieldToKV 为:

func readFieldToKV(fv *reflect.Value, ft *reflect.StructField, kv url.Values) {
    if ft.Anonymous {
        numField := fv.NumField()
        for i := 0; i < numField; i++ {
            ffv := fv.Field(i)
            fft := ft.Type.Field(i)

            readFieldToKV(&ffv, &fft, kv)
        }
        return
    }
    if !fv.CanInterface() {
        return
    }

    tg := readTag(ft, "url")
    if tg.Name() == "-" {
        return
    }

    // 将写 KV 的功能独立成一个函数
    readFieldValToKV(fv, tg, kv)
}

然后我们看看该函数中调用的子函数 readFieldValToKV 的内容,这个函数大概50行,我们分成几块来看:

func readFieldValToKV(v *reflect.Value, tg tags, kv url.Values) {
    key := tg.Name()
    val := ""
    var vals []string
    omitempty := tg.Has("omitempty")
    isSliceOrArray := false

    switch v.Type().Kind() {
    // ......
    // 代码块 1
    // ......

    // ......
    // 代码块 2
    // ......
    }

    // 数组使用 Add 函数
    if isSliceOrArray {
        for _, v := range vals {
            kv.Add(key, v)
        }
        return
    }

    if val == "" && omitempty {
        return
    }
    kv.Set(key, val)
}

其中 代码块1 的内容是将基本类型的数据转为 val string 类型变量值。这没什么好说的,前两篇文章已经解释过了

代码块2 则是对切片和数组的解析,内容如下:

    case reflect.Slice, reflect.Array:
        isSliceOrArray = true
        elemTy := v.Type().Elem()
        switch elemTy.Kind() {
        default:
            // 什么也不做,omitempty 对数组而言没有意义
        case reflect.String:
            vals = readStringArray(v)
        case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
            vals = readIntArray(v)
        case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
            vals = readUintArray(v)
        case reflect.Bool:
            vals = readBoolArray(v)
        case reflect.Float64, reflect.Float32:
            vals = readFloatArray(v)
        }

我们取其中的 readStringArray 为例:

func readStringArray(v *reflect.Value) (vals []string) {
    count := v.Len()                    // Len() 函数

    for i := 0; i < count; i++ {
        child := v.Index(i)                // Index() 函数
        s := child.String()
        vals = append(vals, s)
    }

    return
}

一目了然。这里涉及了 reflect.Value 的两个函数:

  • Len(): 对于切片、数组,甚至是 map,这个函数返回其成员的数量
  • Index(int): 对于切片、数组,这个函数都返回了其成员的位置

后面的操作,就跟标准数字字段一样了,读取 reflect.Value 中的值并返回。

到这里为止的代码,对应 Github 上的 40d0693 版本。读者可以查看 diff 了解相比上一篇文章,为了支持匿名成员和切片/数字类型,我们做了哪些代码改动。

结构体中的结构体

前文已经简单提过了:我们打算用类似点操作符的模式,来处理结构体中的非匿名、可导出的结构体。如果对于 JSON,这种就相当于 “对象中的对象”。

从技术角度,所需的知识其实在前面都已经有了,我们在这一小节中为了支持结构体中的结构体这样的功能,我们需要对源文件做进一步的调整,主要注意的功能点有以下这些:

  • 给相关的函数添加 prefix 参数,支持递归调用以实现多层嵌套
  • 结构体中的结构体的常见模式,包括结构体,以及结构体指针两种情况,需要分别处理

添加 struct in struct 功能的代码版本,则是紧跟着上一版本 070cb3b,读者可以查看 diff 差异,可以看到我的改动其实不多,基本上也就对应着上述两项,短短十来行就实现了对 struct in struct 的支持。

Go map

这是复杂数据类型的最后一个。这里我们说明一下如何从 reflect.Value 中判断对象是否为 map,以及如何从 map 类型的 reflect.Value 中获取 key 和 value 值。

首先我们梳理一下,如果遇到 map 类型的话,我们的判断逻辑:

  1. 首先判断 map 的 key 类型,我们只支持 key 为 string 的 map
  2. 然后判断 map 的 value 类型:

    • 如果是基本数据类型自不必说,支持——比如 map[string]stringmap[string]int 之类的
    • 如果是 struct,也支持,就当作 struct in struct 处理即可
    • 如果是 slice 或者是 array,也按照本文第二小节的处理模式来处理
    • 如果是 interface{} 类型,那么就需要一个个判断每一个值的类型是否支持了

OK,这里我们先介绍 reflect.Value 在处理 map 时所需要使用的几个函数。在能够确定当前 reflect.Value 的 kind 等于 reflect.Map 的前提下:

  • 判断 key 的类型是否为 string:if v.Type().Key().Kind() != reflect.String {return},也就是 reflect.TypeKey() 函数,可以获得 key 的类型。
  • 获得 value 的类型,使用:v.Type().Elem(),返回一个新的 reflect.Type 值,这代表了 map 的 value 的类型。
  • 获得 map 中的所有 key 值,使用:v.MapKeys(),返回一个 []reflect.Value 类型
  • 根据 key 获得 map 中的 value 值:v.MapIndex(k),入参

此外,如果要迭代 map 中的 kv,还可以使用 MapRange 函数,读者可以查阅 godoc

需要添加的代码也不多,在前文 readFieldValToKV 的 “代码块2” 后面再添加一个 “代码块3” 就行,大致如下:

    case reflect.Map:
        if v.Type().Key().Kind() != reflect.String {
            return // 不支持,直接跳过
        }

        keys := v.MapKeys()
        for _, k := range keys {
            subV := v.MapIndex(k)
            if subV.Kind() == reflect.Interface {
                subV = reflect.ValueOf(subV.Interface())
            }
            readFieldValToKV(&subV, tags{k.String()}, kv, key)
        }
        return

为什么要加一句 if subV.Kind() == reflect.Interface 的条件块呢,主要是针对 map[string]interface{} 的支持,因为这种 map 的 value 类型是 reflect.Interface,如果要拿到其底层数据类型的值得,需要再加一句 subV = reflect.ValueOf(subV.Interface()),这样 reflect.Value 的 Kind 才会是其真正的类型。

到这里为止的代码则对应 a18ab4a 版本。至此,通过 reflect 解析结构体的内容就算说明完了。

我们只讲了 marshal 的内容,至于 unmarshal 的过程,在解析参数类型和结构的角度是差不多的,不同的也就只有如何给 interface{} 参数赋值了。笔者争取下一篇文章就写一下这相关的内容吧。

相关文章推荐


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

本文最早发布于云+社区,也是本人的博客

原作者: amc,欢迎转载,但请注明出处。

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

发布日期:2021-09-18

原文链接:https://segmentfault.com/a/1190000040707732


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK