上一篇文章咱们实现了对构造体中根本数据类型的解析。这一篇文章,则是真正令人头疼的、在前两篇文章未解决的几个主题了:

  1. 匿名成员
  2. 构造体中嵌套构造体
  3. Go 切片
  4. Go 数组
  5. 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{} 参数赋值了。笔者争取下一篇文章就写一下这相干的内容吧。

相干文章举荐

  • GO编程模式:切片,接口,工夫和性能
  • 还在用 map「string」interface{} 解决 JSON?通知你一个更高效的办法——jsonvalue
  • Go 语言原生的 json 包有什么问题?如何更好地解决 JSON 数据?
  • 手把手教你用 reflect 包解析 Go 的构造体 - Step 1: 参数类型查看
  • 手把手教你用 reflect 包解析 Go 的构造体 - Step 2: 构造体成员遍历

本文章采纳 常识共享署名-非商业性应用-雷同形式共享 4.0 国内许可协定 进行许可。

本文最早公布于云+社区,也是自己的博客

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

原文题目:《手把手教你用 reflect 包解析 Go 的构造体 - Step 3: 简单类型查看》

公布日期:2021-09-18

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