Golang的反射(reflect)

2022年5月10日 · 3 years ago

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(&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. 参考资料