reflect 是 golang 的一个标准库, 通过反射可以获取变量的类型、值、tag 等,它是实现 gorm、json、yaml 等库的基础. golang 语言通过反射可以在运行时动态的调用对象的方法和属性.
golang reflect 反射的源码位于 golang/go/src/reflect 中,目录下包含以下文件
在源码中, .s 是一些 golang 汇编文件,核心代码 type.go 3155 行代码, value.go 包含 2774 行代码,swapper.go 及 deepequal.go makefunc.go 总计 309 行代码,剩余 go test 源码 7536 余行.
可以看到 reflect 库的核心源码仅 6000 余行, 对这些代码的理解, 有助于更深刻的理解 golang 的 struct 及一些优秀框架、模块的实现原理.
相比与其它语言, golang 的 struct 中可以在类型后增加一段字符串, 即 Tag,在编写程序时,常常通过如下形式对数据进行解析等处理,比如下图所示:
type jsonStruct struct {
A int `json:"a,omitempty"`
}
上面的 struct 中, 变量包含 value、type 及 tag, 其中 tag 内的成员标签变量时 key、value 健值对形式的,参考 StructTag.Lookup(key string) 中获取 tag 中指定 key 的方法
通过 json.Unmarshal 可以将 json 字符串解析到 jsonStruct 类型对象中,同样可以将该类型对象格式化为 json 字符串. 而 json.Unmarshal 及 json.Marshal 都是通过 reflect 实现的.
反射是用来检测存储在变量内部(值 value; 类型 concrete type) pair 对的一种机制. golang reflect 提供了两种类型用于访问接口变量的内容, reflect.ValueOf() 和 reflect.TypeOf(),分别用于获取 pair 中的 value 和 type. 示例如下
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.2345
fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}
运行结果:
type: float64
value: 1.2345
reflect 解决的是运行时获取的对象方法、属性. 在网络中的数据传输,往往时不携带类型信息的字符串、二进制压缩数据. 那么 golang 程序在与其它进程进行通信时,通过 reflect, 便可以很好的完成数据的解析与序列化. 如远程数据库操作、RPC 数据、http 数据、本地 yaml 文件读取等. 相关的这些优质开源库便是通过反射实现,如下面这些 golang 开发中经常用到的框架:
对于刚提到的用了 reflect 的优质开源项目,这里以 gin 框架为例,看看是如何将 reflect 运用上去的. 平时我们写代码时是怎样不知不觉的将反射运用到项目中的.
使用 gin 框架时,经常会通过 Bind() 相关的函数将传入的参数转化为结构体. 下面以 gin.Context.Bind() 为例介绍反射是如何被运用到代码中的.
在 gin-gonic/gin/Context.go 的 Bind() -> MustBindWith() -> ShouldBindWith() -> binding.Bind() 找到 Bind() 方法,它是接口(interface) Binding 的一个方法
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
在 gin-gonic/gin/binding 中, 实现上面 Binding 接口 的 struct 有如下:
从名称可以看到,它们就是 http 传输过程中常用的参数传输的格式.
其中, gin 框架的 form 在实现 Bind() 方法时候,用到了 mapForm 方法,它位于 binding/form_mapping 中的, 而 mapForm 中使用 reflect 实现了 form 数据到 map[string][]string 的转换, 代码如下:
func mapForm(ptr interface{}, form map[string][]string) error {
typ := reflect.TypeOf(ptr).Elem()
val := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
structField := val.Field(i)
if !structField.CanSet() {
continue
}
structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get("form")
inputFieldNameList := strings.Split(inputFieldName, ",")
inputFieldName = inputFieldNameList[0]
var defaultValue string
if len(inputFieldNameList) > 1 {
defaultList := strings.SplitN(inputFieldNameList[1], "=", 2)
if defaultList[0] == "default" {
defaultValue = defaultList[1]
}
}
if inputFieldName == "" {
inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct or struct pointer.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
if structFieldKind == reflect.Ptr {
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structField = structField.Elem()
structFieldKind = structField.Kind()
}
if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form)
if err != nil {
return err
}
continue
}
}
inputValue, exists := form[inputFieldName]
if !exists {
if defaultValue == "" {
continue
}
inputValue = make([]string, 1)
inputValue[0] = defaultValue
}
numElems := len(inputValue)
if structFieldKind == reflect.Slice && numElems > 0 {
sliceOf := structField.Type().Elem().Kind()
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for i := 0; i < numElems; i++ {
if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
return err
}
}
val.Field(i).Set(slice)
continue
}
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
}
return nil
}
对于, json、msgpack、protobuf、xml 等格式的转化,则直接继续调用相关库进行 decode 完成. 具体应用反射的过程,可以进一步查看它们的源码.
在项目中,可能很少会直接使用到 reflect, 但是反射 reflect 作为 golang 40 余个基础库其中之一,使用是及其广泛的,尤其是 golang 的 struct 中经常会用到 Tag 类型,它对应到底层便会使用反射. 另外, 许多优秀的开源程序都会在底层用到 reflect,如上文提到的 gin、gorm、gRPC 等.
理解底层的 reflect, 可以加深对 golang 的结构体 struct 的理解. 在做一些程序设计时,也可在脑海里多提供一种解决问题的思路. 对于各个库之间的关系,也可以更加清晰,有利于梳理技术栈.
开源的框架、组件可能有几百、上千个,但是基础库只有 40 多个,它们反而是实现框架、组建、程序等实现的基础. 掌握 golang 的基础库,再阅读优秀的 golang 源码,可以更快速的理解.