122. Go反射中与结构体相关的常用方法与应用
创始人
2024-11-14 22:33:23
0次

文章目录

  • encoding/json
  • reflect 简介
    • reflect.Value 常用方法
    • reflect.Type 常用方法
  • 应用一:使用 reflect 实现 encoding/json
    • 序列化
    • 反序列化
  • 应用二:使用Tag实现字段级别的访问控制
    • tag 行为自定义
    • 案例:结构体字段访问控制
  • 总结

在使用 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 结构体,结构体包含三个字段,Name、Age 和 Email。

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

Name 字段的 JSON Tag 是 name,则对应的 JSON 字符串的 key 为 name;Age 字段的 JSON Tag 是 age,则对应的 JSON 字符串的 key 为 age;Email 字段没有 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 简介

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

有如下示例代码:

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.ValueOf 和 reflect.TypeOf,它们分别返回 reflect.Value结构体 和 reflect.Type接口 类型。这两个方法可以应用于任何类型对象(any)。

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

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

接下来对 reflect.Value 和 reflect.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 (如SetInt,SetString)方法直接修改 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字段索引为 0,Age 字段索引为 1,Email 字段索引为 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 方法设置结构体字段的值:

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。

首先我们拿到对象 v 的 reflect.Value 和 reflect.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]string,map 的 key 为 JSON 字符串中的 key,map 的 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实现字段级别的访问控制

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

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

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

数据库操作
在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 字段值是否是有效的 email,age 字段是否满足数值在 18 到 99 之间等。

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

示例如下:

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

返回的 valid为 true 或 false,如果发生错误,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。如何使用它呢?

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

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

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

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

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

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

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

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")     } 

相关内容

热门资讯

26年最新开挂版!方片十三张辅... 26年最新开挂版!方片十三张辅助透视挂,清远佬麻将是有外挂,一贯有挂分享1、进入到方片十三张是否有挂...
26年最新开挂版!杭州都莱辅助... 26年最新开挂版!杭州都莱辅助透视挂,腾龙确实有外挂,竟然有挂教学1、首先打开杭州都莱辅助器下载最新...
26年最新开挂版!同城跑胡子合... 26年最新开挂版!同城跑胡子合集辅助透视挂,约战武汉麻将真的有外挂,一直证实有挂1、玩家可以在同城跑...
26年最新开挂版!经典联盟辅助... 26年最新开挂版!经典联盟辅助透视挂,聚友联盟确实有外挂,本来的确有挂1、经典联盟破解器简单,经典联...
26年最新开挂版!同城上饶辅助... 26年最新开挂版!同城上饶辅助透视挂,鹰潭麻将真的是有外挂,都是有挂工具1、同城上饶公共底牌简单,同...
26年最新开挂版!蜀渝牌乐汇辅... 26年最新开挂版!蜀渝牌乐汇辅助透视挂,友友麻将存在有外挂,本来有挂详细1、完成蜀渝牌乐汇有辅助插件...
26年最新开挂版!滇南棋牌辅助... 26年最新开挂版!滇南棋牌辅助透视挂,中至常熟麻将真的有外挂,一直有挂工具1)滇南棋牌免费钻石:进一...
26年最新开挂版!微乐河北麻将... 26年最新开挂版!微乐河北麻将辅助透视挂,雀友圈斗地主真的有外挂,竟然真的有挂1、下载好微乐河北麻将...
26年最新开挂版!渔乐大咖辅助... 26年最新开挂版!渔乐大咖辅助透视挂,福星麻将真的有外挂,原来有挂解密1、下载好渔乐大咖脚本下载之后...
26年最新开挂版!腾龙娱乐辅助... 26年最新开挂版!腾龙娱乐辅助透视挂,边锋老友扎旗麻将是真的有外挂,总是竟然有挂1、金币登录送、破产...