Go 接口与反射的关系
Dec 31, 2018 00:40 · 1472 words · 3 minute read
作为 Go 语言中为抽象而生的基本工具之一,接口在分配值时存储类型信息,而反射则是一种在运行时检查类型和值信息的方法。
reflect
包提供了检查接口甚至在运行时修改值的方法,Go 使用它实现了反射。
向接口分配值
接口打包了三样东西:
- 值
- 方法集
- 存储的值的类型
如下图:
图中可以清楚地看到接口的三个部分:_type
是类型信息,*data
是指向真实值的指针,itab
包含方法集。
当一个函数接受了一个接口作为参数时,传递给函数的接口打包了值、方法集和类型。
通过反射包在运行时检查接口数据
一旦值存储在接口中,可以使用 reflect
包来检查它的各个部分。我们不能直接检查接口结构,而反射包维护一份有权限访问的接口结构的副本。
甚至通过反射对象访问接口,也是和底层接口直接相关的。
reflect.Type
和 reflect.Value
提供访问接口成分的方法。reflect.Type
暴露接口的 _type
部分(有关类型的信息),而 reflect.Value
允许程序员检查和操作值。
reflect.Type (检查类型)
reflect.TypeOf()
函数用来提取值的类型。既然它唯一的参数是空接口,传递给它的参数将被分配成一个接口,因此就有了类型、方法集和值。
reflect.TypeOf()
函数返回 reflect.Type
,它有些方法可以让你查出值的类型。
package main
import (
"fmt"
"log"
"reflect"
)
type Gift struct {
Sender string
Recipient string
Number uint
Contents string
}
func main() {
g := Gift{
Sender: "Hank",
Recipient: "Sue",
Number: 1,
Contents: "Scarf",
}
t := reflect.TypeOf(g)
if kind := t.Kind(); kind != reflect.Struct {
log.Fatalf("This program expects to work on a struct; we got a %v instead.", kind)
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("Field %03d: %-10.10s %v", i, f.Name, f.Type.Kind())
}
}
这段代码的目的是打印 Gift
结构的字段。当 g
作为参数传递给 reflect.TypeOf()
函数,g
就被分配成一个接口,编译器自动填充类型、方法集和值三样东西。这就允许我们遍历接口结构中类型部分的 []fields
:
Field 000: Sender string
Field 001: Recipient string
Field 002: Number uint
Field 003: Contents string
reflect.Method(检查方法集)
reflect.Type
类型也允许访问 itab
部分来提取接口的方法信息。
package main
import (
"log"
"reflect"
)
type Reindeer string
func (r Reindeer) TakeOff() {
log.Printf("%q lifts off.", r)
}
func (r Reindeer) Land() {
log.Printf("%q gently lands.", r)
}
func (r Reindeer) ToggleNose() {
if r != "rudolph" {
panic("invalid reindeer operation")
}
log.Printf("%q nose changes state.", r)
}
func main() {
r := Reindeer("rudolph")
t := reflect.TypeOf(r)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
log.Printf("%s", m.Name)
}
}
这段代码迭代 itab
中存储的数据并展示了每个方法的名称:
Land
TakeOff
ToggleNose
reflect.Value(检查值)
到此为止我们已经讨论了如何查看类型和方法,最后 reflect.Value
提供了接口中存储的真实值的信息。
与 reflect.Value
相关的方法必然将类型信息与真实值联合。举个例子,要提取结构体的字段,反射包必须结合接口的布局信息——尤其是字段的信息和 _type
中存储的字段偏移量还有 *data
指向的真实值。
package main
import (
"fmt"
"log"
"reflect"
)
type Child struct {
Name string
Grade int
Nice bool
}
type Adult struct {
Name string
Occupation string
Nice bool
}
// Search a slice of structs for Name field that is "Hank" and set its Nice field to true.
func nice(i interface{}) {
// Retrieve the underlying value of i. We know that i is an interface.
v := reflect.ValueOf(i)
// we're only interested in slices to let's check what kind of value v is. If it isn't a slice, return immediately.
if v.Kind() != reflect.Slice {
return
}
// v is a slice. Now let's ensure that it is a slice of structs. If not, return immediately.
if e := v.Type().Elem(); e.Kind() != reflect.Struct {
return
}
// Determine if our struct has a Name field of type string and a Nice field of type bool
st := v.Type().Elem()
if nameField, found := st.FieldByName("Name"); found == false || nameField.Type.Kind() != reflect.String {
return
}
if niceField, found := st.FieldByName("Nice"); found == false || niceField.Type.Kind() != reflect.Bool {
return
}
// Set any Nice fields to true where the Name is "Hank"
for i := 0; i < v.Len(); i++ {
e := v.Index(i)
name := e.FieldByName("Name")
nice := e.FieldByName("Nice")
if name.String() == "Hank" {
nice.SetBool(true)
}
}
}
func main() {
children := []Child{
{Name: "Sue", Grade: 1, Nice: true},
{Name: "Ava", Grade: 3, Nice: true},
{Name: "Hank", Grade: 6, Nice: false},
{Name: "Nancy", Grade: 5, Nice: true},
}
adults := []Adult{
{Name: "Bob", Occupation: "Carpenter", Nice: true},
{Name: "Steve", Occupation: "Clerk", Nice: true},
{Name: "Nikki", Occupation: "Rad Tech", Nice: false},
{Name: "Hank", Occupation: "Go Programmer", Nice: false},
}
fmt.Printf("adults before nice: %v", adults)
nice(adults)
fmt.Printf("adults after nice: %v", adults)
fmt.Printf("children before nice: %v", children)
nice(children)
fmt.Printf("children after nice: %v", children)
}
adults before nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer false}]
adults after nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer true}]
children before nice: [{Sue 1 true} {Ava 3 true} {Hank 6 false} {Nancy 5 true}]
children after nice: [{Sue 1 true} {Ava 3 true} {Hank 6 true} {Nancy 5 true}]
注意,nice()
可以更改你传递给它的任意切片的值,不管它接收到了什么类型的参数。
结论
Go 中的反射使用接口和 reflect
反射包实现,没啥黑科技——使用反射时就是直接访问接口里面的内容。
接口更像是一面镜子,允许程序来检查自身。
尽管 Go 是一门静态语言,但是反射和接口相结合非常强大,这个通常动态语言才有。
要想更多关于反射的信息,可以看看包的文档。