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

【手把手教你用|手把手教你用 reflect 包解析 Go 的结构体 - Step 3: 复杂类型检查】上一篇文章我们完成了对结构体中基本数据类型的解析。这一篇文章,则是真正令人头疼的、在前两篇文章未处理的几个主题了:

  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 typeif 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 typereadFieldToKV(&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 := falseswitch 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
手把手教你用|手把手教你用 reflect 包解析 Go 的结构体 - Step 3: 复杂类型检查
文章图片

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

    推荐阅读