Go 反射机制介绍

yzhfd · 2018-03-31 22:57:35 · 6005 次点击 · 预计阅读时间 5 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-03-31 22:57:35 的文章,其中的信息可能已经有所发展或是发生改变。

反射是指一门编程语言可以在运行时( runtime )检查其数据结构的能力。利用 Go 语言的反射机制,可以获取结构体的公有字段以及私有字段的标签名,甚至一些其他比较敏感的信息。

众所周知Go标准库中有一些包利用反射机制来实现它们的功能。我们经常会以 encoding/json 包为例,该包常用来把 JSON 文档解析为结构体,同时也可以把结构体编码为JSON格式的字符串。

本文中我想给大家介绍一个略微有点不一样的例子,该例子是我最近在做的一个聊天项目的消息体,该消息体使用结构体来表示:

type Message struct {
    ID         uint64    `db:"id"`
    Channel    string    `db:"channel"`
    UserName   string    `db:"user_name"`
    UserID     string    `db:"user_id"`
    UserAvatar string    `db:"user_avatar"`
    Message    string    `db:"message"`
    RawMessage string    `db:"message_raw"`
    MessageID  string    `db:"message_id"`
    Stamp      time.Time `db:"stamp"`
}

我想使用 命名 SQL 查询语句把该条消息写入到数据库,正常情况,我应该写一条类似于下面的 SQL 查询语句:

insert into messages set id=:id, channel=:channel,...

然后使用 jmoiron.sqlx 包执行一次 db.NamedExec(query,message) 语句。很显然,随着结构体数量的增加,大量的时间将会浪费在写这些查询语句上面,更糟糕的是,一旦数据库表结构发生变更的话,程序很可能会报错。

试想一下,如果我们能够根据传过来的结构体,自动生成查询语句,会不会是一件很爽的事情?

的确我们可以通过引用 reflect 包来达到我们的上述需求。接下来我将会带着大家一起来领略一下整个实现过程,目前有很多 ORM 包也是使用了跟我类似的方法来达到相同的目的。

把结构体转为 reflect.value 类型

我们需要先创建一个 reflect.Value 的实例,以便于能够获取结构体的字段。同时我们也可以从该实例中获取结构体的函数。创建一个 reflect.Value 实例非常直接:

message_value := reflect.ValueOf(message)

我们需要调用 message_value.NumField() 函数来获取结构体中字段的总数以便于迭代结构体的所有字段。如果我们试图调用 NumField() 的时候传一个 reflect.ValueOf 返回的指针值,程序会产生 panic 错误:

panic:reflect:call of reflect.Value.NumField on ptr Value

为了解决上面这个问题,我们使用 message_value.Kind() 来检查是否是一个指针值,然后得到指针指向的实际的值:

if message_value.Kind() == reflect.Ptr{
    message_value = message_value.Elem()
}

然后我们再调用 message_value.NumField() 就会正确的输出结构体字段的总数。接下来我们将会使用这个值通过循环迭代的方式来获取所有字段的名称和对应的字段值。

读取字段详情

从结构体字段中我们可以获取很多重要的信息,不过我们最感兴趣的还是想要获取字段声明中的标签信息。由于 reflect.Value 是用来处理结构体中每个字段实际存储的值,所以我们需要用 reflect.Type 来获取字段的名称(比如 UserName )或者关联的标签名。

假如我们想要获取结构体所有字段的详细信息列表,详细信息包含字段名称,含有“db”的字段关联的标签名以及字段实际存储的值,代码可以这样写:

message_fields := make([]struct {
    Name  string
    Tag   string
    Value interface{}
}, message_value.NumField())

for i := 0; i < len(message_fileds); i++ {
    fieldValue := message_value.Field(i)
    fieldType := message_value.Type().Field(i)
    message_fields[i].Name = fieldType.Name
    message_fields[i].Value = fieldsValue.Interface()
    message_fields[i].Tag = fieldType.Tag.Get("db")
}

上述代码可以看出每个字段的实际值通过 reflect.Value.Interface() 获取,字段的名称和字段的标签名通过 reflect.Type 获取。你可以在 go playground 上跑一下上述完整的示例。

组合一下功能

其实上述的功能已经完全满足我们的需求了,自动生成 sql 查询语句的关键点在于使用代码来完成字段的标签名的拼接,如下代码所示:

func insert(table string, data interface{})string{
    message_value := reflect.ValueOf(data)
    if message_value.Kind() == reflect.Ptr{
        message_value = message_value.Elem()
    }
    message_fields := make([]string, message_value.NumField())
    for i := 0;i<len(message_fields);i++{
        fieldType := message_value.Type().Field(i)
        message_fields[i] = fieldType.Tag.Get("db")
    }

    sql := "insert into" + table + " set"
    for _,tagFull := range message_fields{
        if tagFull != "" && tagFull != "-"{
            tag := strings.Split(tagFull,",")
            sql = sql + " "+ tag[0]+"=:"+tag[0]+","
        }
    }
    return sql[:len(sql)-1]
}

最终版的代码在这里go playground code

这里还有一些注意事项需要说明一下:

  • 在我们的例子中,由于我们没有深入对结构体进行解析,所以不论结构体的字段是否是指针类型都无关紧要。同时 reflect.Type 信息的获取与字段的实际值也无关。
  • 如果你需要继续对结构体进行解析,一定要注意对指针类型的值进行特殊处理,就像我们前面对 message_value 一样,通过 Elem() 来获取指针值。
  • 还有一些其他非常优秀的包提供了反射机制,如果你想要阅读真实的示例,可以看一下codegangsta/injectfatih/structs这两个包。
  • 推荐大家阅读 The Laws Of Reflection, by Rob Pike

最后提醒一下:如果你之前是一个 PHP 或者 Javascript 程序员,你应该知道在这些语言中缺乏类型安全监测。你可能希望通过反射机制来解决你遇到的一些问题。但是,如果你选择使用 Go 来解决在之前这些语言中遇到的一些使用反射机制解决的问题,我估计你会很痛苦,甚至可能会放弃。

从我以往使用 Go 案例的历史来看,使用反射机制总是磕磕绊绊的。即便是本文中的案例,也是有些牵强的。你完全可以不用使用反射机制,使用字符串切片或者查询语句本身也可以达到上述的目的。如果说你写的 API 接口完全可以满足你的需求,就尽量避免使用反射机制。

案例和观点:即便对于 JSON ,也有一个 json-iterator/go 包用来代替标准库 encoding/json 包。在该包中,大大减少了对反射机制的依赖,速度也得到了显著的提升。反射机制重构的可能性不太大,建议大家使用 gRPC 和 Protobuf 机制。在这个机制中,类型安全可以在代码编写的过程中得到保障。


via: https://scene-si.org/2017/12/21/introduction-to-reflection/

作者:Tit Petric  译者:yzhfd  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

6005 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
1 回复  |  直到 2019-03-11 17:35:40
realksj
realksj · #1 · 6年之前

最终代码的例子,输出结果是: insert into messages set iddfs=:iddfs, channelsdfs=:channelsdfs, user_name=:user_name, user_id=:user_id, user_avatar=:user_avatar, message=:message, message_raw=:message_raw, message_id=:message_id, stamp=:stamp

代码原意是不是应该是输出: insert into messages set id=1, channel=#common, user_name=titpetric, stamp=2019-03-11 17:00:24.2809257 +0800 CST m=+0.002991301,

对应代码为: sql := "insert into " + table + " set" for i := 0; i < len(message_fields); i++ { insVal := message_value.Field(i).Interface() if insVal != "" && insVal != "-" { tag := message_value.Type().Field(i).Tag.Get("db") sql = fmt.Sprintf("%s %s=%v, ", sql, tag, insVal) } }

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传