乐趣区

关于golang:手把手教你用-reflect-包解析-Go-的结构体-Step-2-结构体成员遍历

上一篇文章咱们学习了如何用 reflect 查看一个参数的类型。这一篇文章,咱们取得了一个构造体类型,那么咱们须要探索构造体外部的构造以及其对应的值。

构造体成员迭代

上一篇文章,咱们的 marshal 函数目前是长这个样子:

func marshalToValues(in interface{}) (kv url.Values, err error) {v, err := validateMarshalParam(in)
    if err != nil {return nil, err}

    // ......
}

到这里,咱们拿到了一个 struct 的 reflect.Value 变量。接下来,咱们再增加几行代码,变成这个样子:

func marshalToValues(in interface{}) (kv url.Values, err error) {v, err := validateMarshalParam(in)
    if err != nil {return nil, err}

    t := v.Type()
    numField := t.NumField()

    kv = url.Values{}

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

        // ......
    }

    return kv, nil
}

变量 t 是一个 reflect.Type,示意以后变量的类型,其函数 NumField(),对于 struct 类型变量,则示意该变量下的所有成员字段的数量。

成员解析流程

迭代构造体中的每一个字段,则参见 fv := v.Field(i)ft := t.Field(i)。其中 fv 变量是 reflect.Value 类型,这个通过上一篇文章,读者曾经很相熟了。然而变量 tv 则是 reflect.StructField 类型,这是一个新类型。它示意了字段类型在构造体中的属性。

对于一个构造体成员,除了字段碑身类型之外,咱们还要对其其余属性进行查看,这须要用到 fv 和 ft 变量的几个参数,如下文所示:

匿名成员

Go 的构造体中,反对匿名成员。针对匿名成员的解决,有好几个须要思考的点。此处咱们先略过,后文会再专门阐明,因而代码如下:

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 的构造体中,共有(可导出)成员是大写字母结尾的,而公有(不可导出)成员是小写字母结尾的。依照 Go 的常规,在进行 marshal / unmarshal 操作时,公有成员是不解决的,因而这些成员,咱们该当过滤掉不解决。

然而有一种状况是例外的:匿名成员自身也有可能是不可导出的,这须要辨别解决。所以咱们把匿名成员的解决逻辑放在了后面。因而此时的代码改写为如下所示:

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
        }
        if !fv.CanInterface() { // 是否可导出,应用 fv 变量的 CanInterface 函数进行判断
            continue
        }

        // ......
    }

    return kv, nil
}

Go tag 解析

咱们晓得,在 Go 的很多 marshal / unmarshal 函数中,对构造体变量以及字节流的 key 值的映射,是通过构造体中的标签,也就是 tag 来实现的。比如说上面这个定义:

type Pet struct {
    OwnerID string `url:"owner_id,omitempty" json:"ownerID"`
    Name    string `url:",omitempty"`
    Sex     int
}

就通过这个 tag,将字节流中的 ownerIDOwnerID 变量关联起来。前面的 omitempty 则作为 tag 的额定阐明,示意当字段的值等于空值的时候,则不编码这个字段的值。

至于 Name 字段,因为没有明确指定 tag,那么则默认将其 key 映射为与变量名雷同的 Name

因而,既然咱们本人写 marshal / unmarshal 函数,显然也应该遵循这样的波及模式。咱们写一小段代码来解析这个字段的 tag 信息,入参是 *reflect.StructField 类型,实现以下性能:

  • 如果指定的 tag 配置非空,则分两种状况:

    • 都好之前有内容,那么逗号之前的数据就是 key 名称
    • 逗号之前没有内容,此时用字段的名称作为 tag
  • 如果指定的 tag 配置不存在,则以字段的名称作为 tag
  • 反对获取其余参数
type tags []string

func readTag(ft *reflect.StructField, tag string) tags {tg := ft.Tag.Get(tag)

    // 如果 tag 配置非空,则返回
    if tg != "" {res := strings.Split(tg, ",")
        if res[0] != "" {return res}
        return append(tags{ft.Name}, res[1:]...)
    }
    // 如果 tag 配置为空,则返回字段名
    return tags{ft.Name}
}

// Name 示意以后 tag 所定义的第一个字段,这个字段必须是名称
func (tg tags) Name() string {return tg[0]
}

// Has 判断以后 tag 是否配置了某些额定参数值,比方 omitempty
func (tg tags) Has(opt string) bool {for i := 1; i < len(tg); i++ {t := tg[i]
        if t == opt {return true}
    }
    return false
}

下面的配置,就涵盖了新的 Pet 类型中的几种 tag 状况。

此时,咱们只须要再加一个过滤分支就能够持续往下走了了。这个过滤分支是:当 tag 配置值等于 - 时,依照 Go 的约定,这示意疏忽改字段:

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
        }
        if !fv.CanInterface() { // 是否可导出,应用 fv 变量的 CanInterface 函数进行判断
            continue
        }

        tg := readTag(&ft, "url")
        if tg.Name() == "-" { // - 示意疏忽以后字段
            continue
        }

        // ......
    }

    return kv, nil
}

构造体成员值读取

通过了后面的过滤之后,咱们到这一步,曾经能够取得每个须要解决的、非法的构造体字段信息了,接下来就是获取每一个构造体成员的值。

这一步咱们应用 fv 变量,这是一个 reflect.Value 类型。因为针对不同的数据类型,取值的办法不同。

这里还请读者温习一下 reflect.Kind 类型,在目前阶段,咱们临时先解决以下几种数据类型:

  • 字符串
  • 整型
  • 浮点型
  • 布尔型

至于其余类型则比较复杂,咱们再进一步在后文阐明。

多说无益,这一小段代码并不长,如下所示:

func readFieldVal(v *reflect.Value, tag tags) (s string, ok bool) {switch v.Type().Kind() {
    default:
        return "", false // 不反对的变量类型,间接返回

    case reflect.String:
        return v.String(), true

    case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
        return strconv.FormatInt(v.Int(), 10), true

    case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
        return strconv.FormatUint(v.Uint(), 10), true

    case reflect.Bool:
        return fmt.Sprintf("%v", v.Bool()), true

    case reflect.Float64, reflect.Float32:
        return strconv.FormatFloat(v.Float(), 'f', -1, 64), true
    }
}

代码中展现了针对各种类型地取值函数:

类型 取值函数 备注
字符串 v.String()
无符号整型 v.Uint() 不管位宽多少,对立获取 uint64 类型
有符号整型 v.Int() 不管位宽多少,对立获取 int64 类型
reflect.Bool v.Bool()
浮点数 v.Float() 对立获取 float64 类型

于是,很快啊,咱们的迭代函数的主体,就实现了:

func marshalToValues(in interface{}) (kv url.Values, err error) {v, err := validateMarshalParam(in)
    if err != nil {return nil, err}

    t := v.Type()
    numField := t.NumField() // 构造体下所有字段的数量

    kv = url.Values{}

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

        if ft.Anonymous {
            // TODO: 后文再解决
            continue
        }
        if !fv.CanInterface() {continue}

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

        str, ok := readFieldVal(&fv, tg)
        if !ok {continue}
        if str == ""&& tg.Has("omitempty") {continue}

        // 写 KV 值
        kv.Set(tg.Name(), str)
    }

    return kv, nil
}

验证

咱们写一个简略的 Go test 函数来验证一下:

func TestMarshal(t *testing.T) {
    type Pet struct {
        OwnerID string `url:"owner_id,omitempty" json:"ownerID"`
        Name    string `url:",omitempty"`
        Sex     int
    }

    p := Pet{
        OwnerID: "tencent",
        Name:    "Penguin",
        Sex:     1,
    }

    s, _ := Marshal(&p)
    t.Log(string(s))
}

// 输入
// Name=Penguin&Sex=1&owner_id=tencent

能够看到,输入内容中正确地依照 tag 中的配置,将构造体中的字段序列化为了字节流。

下一步

OK,如果读者的需要中,仅仅须要序列化根本数据类型(字符串、布尔值、数字),那么到这里为止,marshal 函数就能够算是实现了。

然而读者是否还记得咱们在本文中留下了些 TODO 项,这就是咱们在下一篇文章中须要解决的性能了。本文的代码也能够在 Github 上找到,本阶段的代码对应 Commit b2db350。

其余文章举荐

  • 还在用 map[string]interface{} 解决 JSON?通知你一个更高效的办法——jsonvalue
  • Go 语言原生的 json 包有什么问题?如何更好地解决 JSON 数据?
  • 手把手教你用 reflect 包解析 Go 的构造体 – Step 1: 参数类型查看

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

原作者:amc,原文公布于云 + 社区,也是自己的博客。欢送转载,但请注明出处。

原文题目:《手把手教你用 reflect 包解析 Go 的构造体 – Step 2: 构造体成员遍历》

公布日期:2021-06-29

原文链接:https://cloud.tencent.com/developer/article/1336510。

退出移动版