Golang的反射(reflect)

May 10, 2022 at 13:45:07

Golang的反射(reflect)

0. Golang struct 声明时的特殊标记

用 Golang 肯定见过类似这样的标记:

type Model struct {
    ID             string `json:"id"`
    CustomerMobile string `json:"customerMobile"`
    CustomerName   string `json:"customerName"`
}

定义一个 struct 后我们可以在每一个字段(field)后面使用 `` 符号作特殊标记。json: 后面表示这个字段映射到 JSON 数据的自定义 Key。下面这段则是使用 GORM 作数据库字段绑定时的模型定义:

type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

那么这样的字段映射关系到底是如何实现的呢?答案是使用 Golang 的 Reflect(反射)。

1. Golang Reflect

官方文档提供了 reflect 包的所有接口说明,我们可以使用这些接口在 run-time 修改实例对象。通常我们会使用这些接口获取当前实例的值、静态类型,或者使用 TypeOf 接口获取的动态类型。跟多数语言的反射功能类似。

一般我们使用 GORM 一定会有打开数据库的地方:

func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

// Migrate the schema
db.AutoMigrate(&Model{})
//…
}

GORMAutoMigrate() 方法最终会在 schema.go 的实现中,通过 func Parse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) 函数取得上述 &Model{} 这个实例对象,然后再针对它使用 reflect API 获取相关类型信息。以下是关键代码:

// 这里的 dest 就是 &Model{} 这个对象
value := reflect.ValueOf(dest)

// 这里取得 Model 的反射信息
modelType := reflect.Indirect(value).Type()

// 这里通过遍历他的所有 Fields,针对每个 Field 作反射解析
for i := 0; i < modelType.NumField(); i++ {
if fieldStruct := modelType.Field(i); ast.IsExported(fieldStruct.Name) {
if field := schema.ParseField(fieldStruct); field.EmbeddedSchema != nil {
schema.Fields = append(schema.Fields, field.EmbeddedSchema.Fields...)
} else {
schema.Fields = append(schema.Fields, field)
}
}
}

func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field 函数中,获取上述 gorm:"primaryKey" 之类的额外标签信息:

tagSetting = ParseTagSetting(fieldStruct.Tag.Get("gorm"), ";")

如此一来,我们写 struct 声明的时候也就把相应的 ORM 信息一并写了进去。

2. Golang reflect 有什么可玩的?

我们已经知道 TypeOf()ValueOf() 是 reflect 的基础,如果我们要解析的对象是集合类型(如Array, Map等),可以使用 t.Elem() 遍历,如果是 struct 类型,则可用 t.NumField() 循环遍历 t.Feild()

package main

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

type TestData struct {
ID int tag1:"Tag1" tag2:"Tag2"
Title string
}

func main() {
aSlice := []int{1, 2, 3}
aStr := "Hello World!"
aStrPtr := &aStr
aTestData := TestData{ID: 1, Title: "Test"}
aTestDataPtr := &aTestData

aSliceType := reflect.TypeOf(aSlice)
aStrType := reflect.TypeOf(aStr)
aStrPtrType := reflect.TypeOf(aStrPtr)
aTestDataType := reflect.TypeOf(aTestData)
aTestDataPtrType := reflect.TypeOf(aTestDataPtr)

printReflect(aSliceType, 0)
printReflect(aStrType, 0)
printReflect(aStrPtrType, 0)
printReflect(aTestDataType, 0)
printReflect(aTestDataPtrType, 0)

}

func printReflect(t reflect.Type, depth int) {
fmt.Println(strings.Repeat("\t", depth), "Type: (", t.Name(), ") Kind: (", t.Kind(), ")")
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Println(strings.Repeat("\t", depth+1), "Field: (", field.Name, ") Type: (", field.Type, ") Tag: (", field.Tag, ")")
if field.Tag != "" {
fmt.Println(strings.Repeat("\t", depth+2), "Tag is", field.Tag)
fmt.Println(strings.Repeat("\t", depth+2), "tag1 is", field.Tag.Get("tag1"), " tag2 is", field.Tag.Get("tag2"))
}
}
case reflect.Array, reflect.Slice, reflect.Chan, reflect.Map, reflect.Ptr:
fmt.Println(strings.Repeat("\t", depth+1), "Element type: (", t.Elem(), ")")
}

}

上述代码的 gist 在这里。打印出来的结果如下:

 Type: (  ) Kind: ( slice )
         Element type: ( int )
 Type: ( string ) Kind: ( string )
 Type: (  ) Kind: ( ptr )
         Element type: ( string )
 Type: ( TestData ) Kind: ( struct )
         Field: ( ID ) Type: ( int ) Tag: ( tag1:"Tag1" tag2:"Tag2" )
                 Tag is tag1:"Tag1" tag2:"Tag2"
                 tag1 is Tag1  tag2 is Tag2
         Field: ( Title ) Type: ( string ) Tag: (  )
 Type: (  ) Kind: ( ptr )
         Element type: ( main.TestData )

在 run-time 拿到了这些数据之后,我们就可以动态修改他们的值,比如说:

func main() {
    // 声明一个 string
    aStr := "Hello World!"
// 修改它的指针内容
aStrValue := reflect.ValueOf(&amp;aStr)
aStrValue.Elem().SetString("Hello, Goodbye")

// 我们也可以修改一个 struct
aTestData := TestData{ID: 1, Title: "Test"}
aType := reflect.TypeOf(aTestData)

// 手动创建一个新对象
aVal := reflect.New(aType)
aVal.Elem().Field(0).SetInt(2)
aVal.Elem().Field(1).SetString("Test2")
aTestData2 := aVal.Elem().Interface().(TestData)
fmt.Printf("%+v, %d, %s\n", aTestData2, aTestData2.ID, aTestData2.Title)
// 输出如下内容:
// {ID:2 Title:Test2}, 2, Test2

}

除了动态创建对象还可以创建函数。

func MakeTimedFunction(f interface{}) interface{} {
    rf := reflect.TypeOf(f)
    if rf.Kind() != reflect.Func {
        panic("expects a function")
    }
    vf := reflect.ValueOf(f)
    wrapperF := reflect.MakeFunc(rf, func(in []reflect.Value) []reflect.Value {
        start := time.Now()
        out := vf.Call(in)
        end := time.Now()
        fmt.Printf("calling %s took %v\n", runtime.FuncForPC(vf.Pointer()).Name(), end.Sub(start))
        return out
    })
    return wrapperF.Interface()
}

func timeMe() {
fmt.Println("starting")
time.Sleep(1 * time.Second)
fmt.Println("ending")
}

func timeMeToo(a int) int {
fmt.Println("starting")
time.Sleep(time.Duration(a) * time.Second)
result := a * 2
fmt.Println("ending")
return result
}

func main() {
timed := MakeTimedFunction(timeMe).(func())
timed()
timedToo := MakeTimedFunction(timeMeToo).(func(int) int)
fmt.Println(timedToo(2))

// 输出:
// starting
// ending
// calling main.timeMe took 1.001339833s
// starting
// ending
// calling main.timeMeToo took 2.001299666s
// 4

}

reflect还有一堆可以 Make 的东西:

func MakeChan(typ Type, buffer int) Value
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
func MakeMap(typ Type) Value
func MakeMapWithSize(typ Type, n int) Value
func MakeSlice(typ Type, len, cap int) Value

理论上以前在 iOS 上通过 Objective-C Runtime 实现的 JSPatch,也可以在 Golang 使用 reflect 来实现,不过完全没有必要。

3. 所以

可以看到使用 reflect 来构建对象不仅代码写得很绕,而且没有编译器静态检查,很容易写出有问题的代码。目前看来用在 GORM, JSON, YAML 之类的声明是很不错的。ORM 实际上是把一种数据结构转换成另一种,所以我们也可以考虑用 reflect 来实现 AToB() 这样的转换器,而无需显式编写胶水代码。

4. 参考资料

Tags: