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{})
//…
}
GORM
的 AutoMigrate()
方法最终会在 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. 参考资料
- Learning to Use Go Reflection. Post 5 in a Series on Go | by Jon Bodner | Capital One Tech | Medium
- go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly
- GORM Guides | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
- reflect package - reflect - pkg.go.dev