前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你用 reflect 包解析 Go 的结构体 - Step 2: 结构体成员遍历

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

原创
作者头像
amc
修改2021-07-10 11:22:19
2K0
修改2021-07-10 11:22:19
举报
文章被收录于专栏:后台全栈之路后台全栈之路

上一篇文章我们学习了如何用 reflect 检查一个参数的类型。这一篇文章,咱们获得了一个结构体类型,那么我们需要探究结构体内部的结构以及其对应的值。


结构体成员迭代

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

代码语言:go
复制
func marshalToValues(in interface{}) (kv url.Values, err error) {
	v, err := validateMarshalParam(in)
	if err != nil {
		return nil, err
	}

	// ......
}

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

代码语言:go
复制
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 的结构体中,支持匿名成员。针对匿名成员的处理,有好几个需要考虑的点。此处我们先略过,后文会再专门说明,因此代码如下:

代码语言: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 操作时,私有成员是不处理的,因此这些成员,我们应当过滤掉不处理。

但是有一种情况是例外的:匿名成员本身也有可能是不可导出的,这需要区分处理。所以我们把匿名成员的处理逻辑放在了前面。因此此时的代码改写为如下所示:

代码语言: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
		}

		// ......
	}

	return kv, nil
}

Go tag 解析

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

代码语言:go
复制
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
  • 支持获取其他参数
代码语言:go
复制
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 的约定,这表示忽略改字段:

代码语言: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 类型,在目前阶段,我们暂时先处理以下几种数据类型:

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

至于其他类型则比较复杂,我们再进一步在后文说明。

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

代码语言:go
复制
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 类型

于是,很快啊,我们的迭代函数的主体,就完成了:

代码语言:txt
复制
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 函数来验证一下:

代码语言:txt
复制
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

其他文章推荐


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

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

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

发布日期:2021-06-29

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 结构体成员迭代
  • 成员解析流程
    • 匿名成员
      • 不可导出成员
        • Go tag 解析
          • 结构体成员值读取
          • 验证
          • 下一步
          • 其他文章推荐
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档