122. Go反射中与结构体相关的常用方法与应用

在使用 Go 语言开发过程中,我们经常需要实现结构体到 JSON 字符串的序列化(Marshal)或 JSON 字符串到结构体的反序列化(Unmarshal)操作。Go 为我们提供了 encoding/json 库可以很方便的实现这一需求。

encoding/json中就大量用到了反射的知识 60.Go反射库reflect,在本文中,我们将探索如何使用 Go 的反射机制自己来实现一个简易版的 encoding/json 库。这个过程不仅能帮助我们理解序列化和反序列化的基本原理,还能提供一种实用的反射使用方法,加深我们对反射的理解。

通过本文的学习,我们将实现一个能够将结构体与 JSON 字符串互相转换的包。

encoding/json

我们先来回顾下在 Go 中如何使用 encoding/json 库实现结构体和 JSON 字符串互转。

示例代码如下:

package main

import (
 "encoding/json"
 "fmt"
)

type User struct {
 Name  string `json:"name"`
 Age   int    `json:"age"`
 Email string
}

func main() {
 {
  user := User{
   Name:  "测试",
   Age:   20,
   Email: "111@163.com",
  }

  jsonData, err := json.Marshal(user)
  if err != nil {
   fmt.Println("Error marshal to JSON:", err)
   return
  }

  fmt.Printf("JSON data: %s\n", jsonData)
 }

 {
  jsonData := `{"name": "测试", "age": 20, "Email": "111@163.com"}`

  var user User
  err := json.Unmarshal([]byte(jsonData), &user)
  if err != nil {
   fmt.Println("Error unmarshal from JSON:", err)
   return
  }

  fmt.Printf("User struct: %+v\n", user)
 }
}

示例程序中定义了一个 User 结构体,结构体包含三个字段,NameAgeEmail

encoding/json 会根据结构体字段上的 JSON Tag(标签)进行序列化和反序列化。序列化时,JSON Tag 会作为 JSON 字符串的 key,字段值作为 JSON 字符串的 value。反序列化时,JSON 字符串的 key 所对应的值会被映射到具有同样 JSON Tag 的结构体字段上。

Name 字段的 JSON Tag name,则对应的 JSON 字符串的 keynameAge 字段的 JSON Tagage,则对应的 JSON 字符串的 keyageEmail 字段没有 JSON Tag,则默认会使用字段名 Email 作为对应的 JSON 字符串 key

执行示例代码,得到如下输出:

$ go run main.go
JSON data: {"name":"测试","age":20,"Email":"111@163.com"}
User struct: {Name:测试 Age:20 Email:111@163.com}

reflect 简介

reflectGo 语言为我们提供的反射库,用于在运行时检查类型并操作对象。它是实现动态编程和元编程的基础,使程序能够在运行时获取类型信息并进行相应的操作。

有如下示例代码:

package main

import (
 "fmt"
 "reflect"
)

type User struct {
 Name  string `json:"name"`
 Age   int    `json:"age"`
 Email string
}

func main() {
 // 内置类型
 {
  age := 20

  val := reflect.ValueOf(age)
  typ := reflect.TypeOf(age)
  fmt.Println(val, typ)

 // 自定义结构体类型
 {
  user := User{
   Name:  "测试",
   Age:   20,
   Email: "111@163.com",
  }

  val := reflect.ValueOf(user)
  typ := reflect.TypeOf(user)
  fmt.Println(val, typ)
 }
}

执行示例代码,得到如下输出:

$ go run main.go
20 int
{测试 20 111@163.com} main.User

reflect 最常用的两个方法分别是 reflect.ValueOfreflect.TypeOf,它们分别返回 reflect.Value结构体reflect.Type接口 类型。这两个方法可以应用于任何类型对象(any)。

  • reflect.Value:表示一个 Go 值,它提供了一些方法,可以获取值的详细信息,也可以操作值,例如获取值的类型、设置值等。

  • reflect.Type:表示一个 Go 类型,它提供了一些方法,可以获取类型的详细信息,例如类型的名称(Name)、种类(Kind,基本类型、结构体、切片等)。

接下来对 reflect.Valuereflect.Type 类型的常用方法进行介绍,以如下实例化 User 结构体指针作为被操作对象:

// 实例化 User 结构体指针
user := &User{
 Name:  "测试",
 Age:   20,
 Email: "111@163.com",
}

reflect.Value 常用方法

reflect.Value 提供了 Kind 方法可以获取对应的类型类别:

// 注意这里传递的是指针类型
kind := reflect.ValueOf(user).Kind()
fmt.Println(kind)
kind = reflect.ValueOf(*user).Kind()
fmt.Println(kind)
kind = reflect.ValueOf(user).Elem().Kind()
fmt.Println(kind)

这段示例代码将得到如下输出:

ptr
struct
struct

这里 Kind 方法返回的是 User 的底层类型 struct,以及 ptr 类型,ptr 代表指针类型。

值得注意的是,如果传递给 reflect.ValueOf 的是指针类型(user),需要使用 Elem 方法获取指针指向的值;如果传递给 reflect.ValueOf 的是值类型(*user),则可以直接得到值。

使用指针类型的好处是可以使用 reflect.Value 提供的 Set<Type> (如SetIntSetString)方法直接修改 user 字段的值,稍后讲解。

reflect.Value 同样提供了 Type 方法,可以得到 reflect.Type

// 以下二者等价
tpy := reflect.ValueOf(user).Type()
fmt.Println(tpy)
tpy1 := reflect.TypeOf(user)
fmt.Println(tpy1)
fmt.Println(reflect.DeepEqual(tpy, tpy1))

这与 reflect.TypeOf 等价。

这段示例代码将得到如下输出:

*main.User
*main.User
true

我们有多种方式可以获取结构体值字段:

nameField := reflect.ValueOf(user).Elem().FieldByName("Name")
ageField := reflect.ValueOf(user).Elem().FieldByIndex([]int{1})
emailField := reflect.ValueOf(user).Elem().Field(2)
  • FieldByName 方法可以通过字段名获取结构体字段。

  • FieldByIndex 方法通过索引切片获取结构体字段。

  • Field 方法通过索引获取结构体字段。

实际上 FieldByIndex 方法内部调用的也是Field方法。这里的索引是结构体字段按照顺序排序所在位置,即Name字段索引为 0Age 字段索引为 1Email 字段索引为 2

我们可以使用 NumField 获取结构体字段总个数:

numField := reflect.ValueOf(*user).NumField()
fmt.Println(numField)

拿到结构体字段对象后,可以根据其具体类型获取对应值:

fmt.Println(nameField.String())
fmt.Println(ageField.Int())
fmt.Println(emailField.String())

以上示例代码将得到如下输出:

3
测试
20
111@163.com

因为我们传递给 reflect.ValueOf 函数的是 User 结构体指针,所以可以使用 reflect.Value 提供的 Set<Type> 方法设置结构体字段的值:

nameField.SetString("ceshi")             // 设置 Name 字段的值
ageField.SetInt(18)                               // 设置 Age 字段的值
emailField.SetString("123@163.com") // 设置 Email 字段的值

现在打印 user 对象:

fmt.Println(user)

得到输出:

&{ceshi 18 123@163.com}

如果我们传递给 reflect.ValueOf 函数的不是 User 结构体指针,而是结构体对象:

nameField := reflect.ValueOf(*user).FieldByName("Name")

现在去设置字段值:

nameField.SetString("ceshi")

程序会直接 panic

panic: reflect: reflect.Value.SetString using unaddressable value

此外,我们还可以总结一个规律,使用指针时,就需要通过 Elem 方法获取指针指向的值,不使用指针就不需要调用 Elem 方法。

reflect.Type 常用方法

现在我们再来简单介绍下 reflect.Type 的几个常用方法。

reflect.Type 同样提供了如下几个方法,与 reflect.Value 对应:

nameField, _ := reflect.TypeOf(user).Elem().FieldByName("Name")
ageField := reflect.TypeOf(user).Elem().FieldByIndex([]int{1})
emailField := reflect.TypeOf(user).Elem().Field(2)

我们来输出下这几个对象的值:

fmt.Printf("%+v\n", nameField)
fmt.Printf("%+v\n", ageField)
fmt.Printf("%+v\n", emailField)

得到如下输出:

{Name:Name PkgPath: Type:string Tag:json:"name" Offset:0 Index:[0] Anonymous:false}
{Name:Age PkgPath: Type:int Tag:json:"age" Offset:16 Index:[1] Anonymous:false}
{Name:Email PkgPath: Type:string Tag: Offset:24 Index:[2] Anonymous:false}

这里打印了结构体每个字段的信息。

  • Name 对应字段名。

  • PkgPath 是包路径。

  • Type 是结构体字段类型。

  • Tag 即为字段标签。

  • Offset 是字段偏移量。结构体内存对齐可能用到该偏移量。

  • Index 是字段索引位置。

  • Anonymous 表示是否为匿名字段。比如如下结构体:

type User struct {
 Name  string `json:"name"`
 Age   int    `json:"age"`
 Email string
 string
}

这个结构体定义中,最后一个字段就是匿名字段。

现在我们想获取结构体字段 JSON Tag,可以这样做:

tag := nameField.Tag
fmt.Printf("%+v\n", tag)
fmt.Printf("%+v\n", tag.Get("json"))

将得到如下输出:

json:"name"
name

reflect 基础语法就讲解到这里,更多使用方法需要我们在以后的的实践中去探索。

应用一:使用 reflect 实现 encoding/json

接下来就看看,如何使用 reflect 自己实现一个简易版本的 encoding/json

示例程序目录结构如下:

$ tree                    
.
├── encoding
│   └── json
│       ├── decode.go
│       └── encode.go
├── go.mod
└── main.go
  • encoding/json/encode.go 用于实现序列化功能。

  • encoding/json/decode.go 用于实现反序列化功能。

  • main.go 用来验证这个简易版的 encoding/json 功能。

序列化

首先是实现序列化的代码:

package json

import (
 "fmt"
 "reflect"
 "strconv"
 "strings"
)

// Marshal 序列化
func Marshal(v any) (string, error) {
 // 拿到对象 v 的 reflect.Value 和 reflect.Type
 val := reflect.ValueOf(v)
 if val.Kind() != reflect.Struct {
  return "", fmt.Errorf("only structs are supported")
 }
 typ := val.Type()

 // 用来保存 JSON 字符串
 jsonBuilder := strings.Builder{}

 // NOTE: 三步走拼接 JSON 字符串
 // 1. JSON 左花括号
 jsonBuilder.WriteString("{")

 // 2. key/value
 for i := 0; i < val.NumField(); i++ {
  fieldVal := val.Field(i)
  fieldType := typ.Field(i)

  // 获取 JSON 标签
  // 与类型相关的信息都在reflect.Type对象中
  tag := fieldType.Tag.Get("json")
  if tag == "" {
   tag = fieldType.Name
  }

  jsonBuilder.WriteString("\"" + tag + "\"")

  // 根据字段类型转换,仅支持 string/int
  switch fieldVal.Kind() {
  case reflect.String:
   jsonBuilder.WriteString(`"` + fieldVal.String() + `"`)
  case reflect.Int:
   jsonBuilder.WriteString(strconv.FormatInt(fieldVal.Int(), 10))
  default:
   return "", fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
  }

  if i < val.NumField()-1 {
   jsonBuilder.WriteString(",")
  }
 }

 // 3. JSON 右花括号
 jsonBuilder.WriteString("}")

 return jsonBuilder.String(), nil
}

这段代码中没有新的 reflect 语法,我们都在前文中介绍了,这里捋一下代码逻辑。

所谓序列化操作,就是 Go 结构体转 JSON 字符串的操作。

这里函数名参考 encoding/json 同样被定义为 Marshal

首先我们拿到对象 vreflect.Valuereflect.Type,待后续使用。

接着使用strings.Builder构造了一个用来保存JSON字符串信息的对象 jsonBuilder

构造 JSON 字符串分三步走:

先写入JSON左花括号{内容到 jsonBuilder

根据结构体字段和值,构造 JSON 字符串的键值对 key/value 并写入 jsonBuilder

最后写入 JSON 右花括号}内容到 jsonBuilder

函数最终返回 jsonBuilder.String() 即为 JSON 字符串。

这里面主要逻辑都在步骤2中。

首先会遍历结构体每个字段,并使用如下方式获取每个字段对应的 JSON Tag

tag := fieldType.Tag.Get("json")
if tag == "" {
 tag = fieldType.Name
}

JSON Tag 不存在,则默认使用结构体字段名作为 JSON 字符串的 key,比如 User.Email 字段。

JSON key: 写入 jsonBuilder

jsonBuilder.WriteString(`"` + tag + `":`)

然后根据结构体字段类型转换成对应的 JSON 数据类型,写入 jsonBuilder

switch fieldVal.Kind() {
case reflect.String:
 jsonBuilder.WriteString(`"` + fieldVal.String() + `"`)
case reflect.Int:
 jsonBuilder.WriteString(strconv.FormatInt(fieldVal.Int(), 10))
default:
 return "", fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
}

每次循环末尾,判断是否为结构体最后一个字段,如果不是,则写入分隔符 ,:

if i < val.NumField()-1 {
 jsonBuilder.WriteString(",")
}

至此,序列化代码逻辑大功告成。

我们可以使用如下示例代码进行测试:

user := User{
 Name:  "测试",
 Age:   20,
 Email: "111@163.com",
}

jsonData, err := simplejson.Marshal(user)
if err != nil {
 fmt.Println("Error marshal to JSON:", err)
 return
}

fmt.Printf("JSON data: %s\n", jsonData)

执行示例代码,得到如下输出:

$ go run main.go
JSON data: {"name":"测试","age":20,"Email":"111@163.com"}

没有任何问题,与原生的 encoding/json 中的 Marshal 方法表现一致。

反序列化

接下来是实现反序列化的代码:

package json

import (
 "errors"
 "fmt"
 "reflect"
 "strconv"
 "strings"
)

// Unmarshal 反序列化
func Unmarshal(data []byte, v interface{}) error {
 // 将json字节数组转为map[string]string  key:json中的key,value:json中的value
 parsedData, err := parseJSON(string(data))
 if err != nil {
  return err
 }

 if reflect.TypeOf(v).Kind() != reflect.Ptr{
	return errors.New("args v not ptr")
 }
 val := reflect.ValueOf(v).Elem()
 typ := val.Type()

 for i := 0; i < val.NumField(); i++ {
  fieldVal := val.Field(i)
  fieldType := typ.Field(i)

  // 获取 JSON 标签
  tag := fieldType.Tag.Get("json")
  if tag == "" {
   tag = fieldType.Name
  }

  // 从解析的数据中获取值
  if value, ok := parsedData[tag]; ok {
   switch fieldVal.Kind() {
   case reflect.String:
    fieldVal.SetString(value)
   case reflect.Int:
    intValue, err := strconv.Atoi(value)
    if err != nil {
     return err
    }
    fieldVal.SetInt(int64(intValue))
   default:
    return fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
   }
  }
 }

 return nil
}

这段代码中同样没有新的 reflect 语法。

所谓反序列化操作,就是 JSON 字符串转Go结构体的操作。

这里函数名参考 encoding/json 同样被定义为 Unmarshal,并且函数签名也保持一致。

反序列化操作首先使用 parseJSON 函数解析传递进来的 JSON 数据,得到 parsedData

parsedData 类型为 map[string]stringmapkey JSON 字符串中的 keymap value 即为 JSON 字符串中的 value

接下来核心逻辑是遍历结构体每个字段,并获取字段对应的 JSON Tag

tag := fieldType.Tag.Get("json")
if tag == "" {
 tag = fieldType.Name
}

JSON Tag 不存在,则默认使用结构体字段名作为 JSON 字符串的 key,比如 User.Email 字段。

然后根据JSON Tag从解析后的parsedData数据中获取 key/value

if value, ok := parsedData[tag]; ok {
 switch fieldVal.Kind() {
 case reflect.String:
  fieldVal.SetString(value)
 case reflect.Int:
  intValue, err := strconv.Atoi(value)
  if err != nil {
   return err
  }
  fieldVal.SetInt(int64(intValue))
 default:
  return fmt.Errorf("unsupported field type: %s", fieldVal.Kind())
 }
}

这里根据结构体字段的类型,将 parsedData 中对应的字符串 value 转换成对应类型。并使用 reflect.Value 提供的 SetString SetInt 方法设置字段的值。

现在,我们唯一没有讲解的逻辑就只剩下 parseJSON 函数了。

parseJSON 函数定义如下:

// 简易版 JSON 解析器,仅支持 string/int 且不考虑嵌套
func parseJSON(data string) (map[string]string, error) {
 result := make(map[string]string)

 data = strings.TrimSpace(data)
 if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
  return nil, errors.New("invalid JSON")
 }

 data = data[1 : len(data)-1]
 parts := strings.Split(data, ",")
 for _, part := range parts {
  kv := strings.SplitN(part, ":", 2)
  if len(kv) != 2 {
   return nil, errors.New("invalid JSON")
  }

  k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
  v := strings.Trim(strings.TrimSpace(kv[1]), `"`)

  result[k] = v
 }

 return result, nil
}

parseJSON 实现了一个简易版本的JSON字符串解析器,能够将JSON字符串的key/value解析出来,并保存到 map[string]string 中。

我们可以使用如下示例代码进行测试Unmarshal代码逻辑是否正确:

jsonData := `{"name": "测试", "age": 20, "Email": "111@163.com"}`

var user User
err := simplejson.Unmarshal([]byte(jsonData), &user)
if err != nil {
 fmt.Println("Error unmarshal from JSON:", err)
 return
}

fmt.Printf("User struct: %+v\n", user)

执行示例代码,得到如下输出:

$ go run main.go
User struct: {Name:测试 Age:20 Email:111@163.com}

没有任何问题,与原生的 encoding/json 中的 Unmarshal 方法表现一致。

应用二:使用Tag实现字段级别的访问控制

与其他语言对比的话,虽然 Gostruct tag 在某种程度上类似于 Java 的注解或C#的属性,但 Gotag更加简洁,并且主要通过反射机制在运行时被访问。

结构体 tagGo 语言中常见用途,平时最常见有如下这些。

JSON/XML 序列反序列化
如前面的介绍的案例中,通过 encoding/json 或者其他的库如 encoding/xml 库,tag 可以控制如何将结构体字段转换为 JSONXML,或者如何从它们转换回来。

数据库操作
在ORM(对象关系映射)库中,tag 可以定义数据库表的列名、类型或其他特性。

如我们在使用 Gorm 时,会看到这样的定义:

type User struct {
    gorm.Model
    Name   string `gorm:"type:varchar(100);unique_index"`
    Age    int    `gorm:"index:age"`
    Active bool   `gorm:"default:true"`
}

结构体 tag 可用于定义数据库表的列名、类型或其他特性。

数据验证
在一些库中,tag 用于验证数据,例如,确保一个字段是有效的电子邮件地址。

如下是 govalidator使用结构体上 tag 实现定义数据验证规则的一个案例。

type User struct {
    Email string `valid:"email"`
    Age   int    `valid:"range(18|99)"`
}

在这个例子中,valid tag 定义了字段的验证规则,如 email 字段值是否是有效的 emailage 字段是否满足数值在 18 99 之间等。

我们只要将类型为 User 类型的变量交给 govalidator,它可以根据这些规则来验证数据,确保数据的正确性和有效性。

示例如下:

valid, err := govalidator.ValidateStruct(User{Email: "test@example.com", Age: 20})

返回的 valid truefalse,如果发生错误,err 提供具体的错误原因。

tag 行为自定义

前面展示的都是利用标准库或三方库提供的能力,如果想自定义 tag 该如何实现?毕竟有些情况下,如果默认提供的tag提供的能力不满足需求,我们还是希望可以自定义tag的行为。

这需要了解与理解 Go 的反射机制,它为数据处理和元信息管理提供了强大的灵活性。

如下的示例代码:

type Person struct {
    Name string `mytag:"MyName"`
}

t := reflect.TypeOf(Person{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag.Get("mytag")) // 输出: MyName

在这个例子中,我们的 Person 的字段 Name 有一个自定义的 tag - mytag,我们直接通过反射就可以访问它。

这只是简单的演示如何访问到 tag。如何使用它呢?

这就要基于实际的场景了,当然,这通常也离不开与反射配合。下面我们来通过一个实际的例子介绍。

案例:结构体字段访问控制

让我们考虑一个实际的场景:一个结构访问控制系统。

这个系统中,我们可以根据用户的角色(如 adminuser)或者请求的来源(adminweb)控制对结构体字段的访问。具体而言,假设我定义了一个包含敏感信息的结构体,我可以使用自定义 tag 来标记每个字段的访问权限。

是不是想到,这或许可用在 API 接口范围字段的控制上,防止泄露敏感数据给用户。

接下来,具体看看如何做吧?

定义结构体
我们首先定义一个UserProfile结构体,其中包含用户的各种信息。每个信息字段都有一个自定义的 access tag,用于标识字段访问权限(adminuser)。

type UserProfile struct {
    Username    string `access:"user"`  // 所有用户可见
    Email       string `access:"user"`  // 所有用户可见
    PhoneNumber string `access:"admin"` // 仅管理员可见
    Address     string `access:"admin"` // 仅管理员可见
}

其中,PhoneNumber Address是敏感字段,它只对 admin 角色可见。而 UserName Email 则是所有用户可见。

到此,结构体UserProfile定义完成。

实现权限控制
接下来就是要实现一个函数,实现根据 UserProfile 定义的 access tag 决定字段内容的可见性。

假设函数名称为 FilterFieldsByRole,它接受一个 UserProfile 类型变量和用户角色,返回内容一个过滤后的 map(由 fieldname fieldvalue 组成的映射),其中只包含角色有权访问的字段。

func FilterFieldsByRole(profile UserProfile, role string) map[string]string {
    result := make(map[string]string)
    val := reflect.ValueOf(profile)
    typ := val.Type()

    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        accessTag := field.Tag.Get("access")
        if accessTag == "user" || accessTag == role {
            // 获取字段名称
            fieldName := strings.ToLower(field.Name) 
            // 获取字段值
            fieldValue := val.Field(i).String() 
            // 组织返回结果 result
            result[fieldName] = fieldValue
        }
    }
    return result
}

权限控制的重点逻辑部分,就是 if accessTag == "user" || accessTag == role 这段判断条件。当满足条件之后,接下来要做的就是通过反射获取字段名称和值,并组织目标的 Map 类变量 result了。

使用演示
让我们来使用下FilterFieldsByRole函数,检查下是否满足按角色访问特定的用户信息的功能。

示例代码如下:

func main() {
    profile := UserProfile{
        Username:    "johndoe",
        Email:       "johndoe@example.com",
        PhoneNumber: "123-456-7890",
        Address:     "123 Elm St",
    }

    // 假设当前用户是普通用户
    userInfo := FilterFieldsByRole(profile, "user")
    fmt.Println(userInfo)

    // 假设当前用户是管理员
    adminInfo := FilterFieldsByRole(profile, "admin")
    fmt.Println(adminInfo)
}

输出:

map[username:johndoe email:johndoe@example.com]
map[username:johndoe email:johndoe@example.com phonenumber:123-456-7890 address:123 Elm St]

这个场景,通过自定义结构体 tag,给予指定角色,很轻松地就实现了一个基于角色的权限控制。

毫无疑问,这个代码更加清晰和可维护,而且具有极大灵活性、扩展性。如果想扩展更多角色,也是更加容易。

不过还是要说明下,如果在API层面使用这样的能力,还是要考虑反射可能带来的性能影响。

总结

这篇博文介绍了Go语言中结构体 tag 的基础知识,如是什么,如何使用。另外,还介绍了它们在不同场景下的应用。通过简单的例子和对比,我们看到了Go中结构体tag的作用。

reflect 最常用的两个方法分别是 reflect.ValueOf reflect.TypeOf,调用这两个方法分别可以得到 reflect.Value 结构体reflect.Type接口 类型。

有了这两个类型及其方法,我们可以获取任意一个 Go 对象的类型信息、值的详细信息和操作值,可见反射之强大。

文章的最后,通过两个实际案例,演示了如何使用 struct tag 使我们代码更加灵活强大。 struct tag 的使用不仅非常直观,而且正确地利用这些 tag 可以极大提升我们程序的功能和效率。

总结遍历操作结构体的常用代码:

	// 获取结构体的Value和Type
	val := reflect.ValueOf(s)
	typ := val.Type()

	// 遍历结构体字段
   for i := 0; i < val.NumField(); i++ {
   	   // 获取到字段对应的Value和StructField(包含字段的类型信息,如字段名、Tag,类型,路径,是否匿名等)
   	   fieldVal := val.Field(i)
       fieldType := typ.Field(i)
       //进行相应的后续处理
       //如 xxxTag := fieldType.Tag.Get("xxx")

   }
  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值