元编程是一个很有意思东西,正好golang中包含了一个reflect包提供反射功能,上周末看了看reflect包,在此记录一下学习体会。
元编程
首先,了解一下元编程及其相关概念。元编程是什么?元编程就是普通的编程。那么为什么会有元编程这个概念呢?这是因为元编程会做出一些比普通编程行为更酷的行为,它能够通过写好的代码来处理代码,因此给它起了一个新名字以示区分。听起来有些矛盾和拗口,下面来看一个例子说明一下吧。
有过Qt或者MFC编程经验的人都知道,如果我们想要给按钮或者其他控件添加对某个事件的响应处理函数,只需要按照一定的明明规范来命名一个函数即可。例如在Qt中,想要对一个名叫btn的按钮绑定一个点击事件的处理函数只需要位该函数命名为on_btn_clicked即可。但是,我们并没有显示的将该函数和btn连接在一起,这是怎么做到的呢?Qt中一些代码会分析函数名,当检测到以on_objectname_signalname命名的函数时,Qt会自动将该函数和objectname对象的signalname信号绑定。这就是元编程,通过代码来对其他代码进行处理。
反射(reflection)和内省(type introspection)
反射和内省是和元编程息息相关的两个概念,这两个概念非常相似,但却有很大的不同。
**内省**是在运行时检查对象的类型和属性的能力,**反射**是在运行时检查和修改程序结构和行为的能力。
可以看出,反射比内省更加强大,内省是反射的子集,这一点不应该混淆。有些为地方可以看到说自省和反射是一回事,我想这个自省应该是根据introspection来翻译的,而introspection是type introspection的简称。so,不要相信那些人,他们应该去补习了。
golang reflect
在golang中,提供了一个reflect包,这个包主要包含了两个主要类型Type和Value,并且在reflect包中还提供了两个非常方便的函数TypeOf和ValueOf来分别获得这两种类型。下面来看一段示例代码:
package main
import (
"fmt"
"reflect"
"regexp"
)
type Struct struct {
Pub string
pri int `pri:"private"`
}
func (s *Struct) Pri() int {
return s.pri
}
func (s *Struct) sum(o int) int {
return s.pri + o
}
func (s *Struct) Sum(o int) int {
s.pri = s.sum(o)
return s.pri
}
func (s *Struct) Name(firstName, lastName string) string {
return firstName + " " + lastName
}
func sum(a, b int) int {
return a + b
}
func main() {
s := &Struct{}
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
fmt.Println("Type:", t)
fmt.Println("Value:", v)
fmt.Println("Kind:", t.Kind())
for i := 0; i < t.Elem().NumField(); i++ {
f := t.Elem().Field(i)
fmt.Printf("struct field %d: %s, %s, embeded?: %v, tag: %v\n", i, f.Name, f.Type, f.Anonymous, f.Tag)
}
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("struct field %d: %s, %s\n", i, m.Name, m.Type)
}
callMethod := func(s reflect.Value, methodName string, methodArgs ...reflect.Value) ([]reflect.Value, error) {
t := s.Type()
method, exist := t.MethodByName(methodName)
if !exist {
return nil, fmt.Errorf("\"%s\": is not existed for %s", methodName, t)
}
if regexp.MustCompile(`^[a-z]`).MatchString(method.Name) {
return nil, fmt.Errorf("\"%s\": unexported field cannot be called", method.Name)
}
args := []reflect.Value{s}
args = append(args, methodArgs...)
return method.Func.Call(args), nil
}
fmt.Print("call Struct.Pri: ")
fmt.Println(callMethod(v, "Pri"))
fmt.Print("call Struct.Sum: ")
fmt.Println(callMethod(v, "Sum", reflect.ValueOf(1)))
fmt.Print("call Struct.Name: ")
fmt.Println(callMethod(v, "Name", reflect.ValueOf("David"), reflect.ValueOf("Beckham")))
fmt.Print("call Struct.sum: ")
fmt.Println(callMethod(v, "sum", reflect.ValueOf(1)))
fmt.Print("call Struct.s: ")
fmt.Println(callMethod(v, "s"))
fn := reflect.ValueOf(sum)
ft := fn.Type()
for i := 0; i < ft.NumIn(); i++ {
in := ft.In(i)
fmt.Printf("function argument %d: %s\n", i, in)
}
for i := 0; i < ft.NumOut(); i++ {
out := ft.In(i)
fmt.Printf("function return value %d: %s\n", i, out)
}
i, j := 1, 3
fmt.Printf("Call sum(%d, %d) function: %v\n",
i, j,
fn.Call([]reflect.Value{
reflect.ValueOf(i),
reflect.ValueOf(j),
})[0].Interface(),
)
}如果运行这段代码会得到一下输出:
Type: *main.Struct
Value: <*main.Struct Value>
Kind: ptr
struct field 0: Pub, string, embeded?: false, tag:
struct field 1: pri, int, embeded?: false, tag: pri:"private"
struct field 0: Name, func(*main.Struct, string, string) string
struct field 1: Pri, func(*main.Struct) int
struct field 2: Sum, func(*main.Struct, int) int
struct field 3: sum, func(*main.Struct, int) int
call Struct.Pri: [<int Value>] <nil>
call Struct.Sum: [<int Value>] <nil>
call Struct.Name: [David Beckham] <nil>
call Struct.sum: [] "sum": unexported field cannot be called
call Struct.s: [] "s": is not existed for *main.Struct
function argument 0: int
function argument 1: int
function return value 0: int
Call sum(1, 3) function: 4代码分析
这段代码虽然有点长,不过却非常简单和清晰,现在来分析一下这段代码及其输出结果。
在代码的开始部分,通过reflect.ValueOf来获取Struct结构体指针的值信息,通过reflect.TypeOf来获取Struct结构体指针的类型信息,需要留意的地方是,这里创建的是一个Struct的结构体指针,而不是一个Struct结构体。
Kind
在代码的第47行调用了一个叫Kind的方法,Kind方法是用来表示一个Type是属于哪一种类型的,因此,通过Kind方法也可以准确的判断出这是一个ptr。
Elem
在遍历结构体字段时用到了一个叫Elem的方法,由于s是一个指针类型,因此需要通过Value.Elem得到结构体的值信息,这样才能获取Struct结构体中的字段信息。同时需要注意,Value.Elem方法只对指针和接口——例如error——有效。
在reflect.Value和reflect.Type中有不少命名相同的方法,不过意义却不一样,Elem就是一个典型的例子,与Value.Elem不同,在reflect.Type中Type.Elem用来可以用来表示map中值的类型。
struct tag
在golang中有一个被成为struct tag的东西,每次见到这个东西都感觉怪怪的,通常也很少使用。
struct tag通常是一个用空格分隔的键值对,在键中不包含双引号,冒号和空格,而值则是由双引号引起来的任意字符。
不过鉴于struct tag是一个字符串,而reflect.StructTag本身其实也是一个字符串,也许可以在适当的时候自由发挥一下。另外,reflect.StructTag只有一个Get方法。
下面是来自reflect包文档中的一个例子,更详细的展示了struct tag的用法:
package main
import (
"fmt"
"reflect"
)
func main() {
type S struct {
F string `species:"gopher" color:"blue"`
}
s := S{}
st := reflect.TypeOf(s)
field := st.Field(0)
fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
}Method and Call
接下来是callMethod函数。在callMethod函数中有几个地方需要注意。
首先是通过reflect.Type中的MethodByName方法来获取reflect.Method类型的方法信息,以及判断该方法是否存在。
如果你愿意,同样可以使用reflect.Value中的MethodByName方法来获得reflect.Value类型的方法信息,然后通过Value.IsValid来判断该方法是否存在。这里的reflect.Value类型的信息等于reflect.Method类型中的Func字段。
第二个需要注意的地方时这里通过使用正则表达式来判断方法的名字的首字母大小写来判断该方法是否导出,这是由于reflect包中似乎并没有提供可以判断方法和字段是否导出的方法。
第三,在调用一个结构体方法时,需要将receiver作为第一个参数传递个方法。
Value.Type()
在对sum进行反射时并没有使用reflect.TypeOf,而是使用了Value.Type方法,这个方法同样可以得到类型信息。
Interface
在代码的最后一部分取得返回值时使用了Value.Interface方法。该方法的作用是返回一个interface{}以便能够获取真正的值。
改变对象的值
reflect包出了获取各种信息以外,还可以改变变量的值。一个对象能否设值,可以通过Value.CanSet方法来判断。不过需要记住,只有指针类型通过Elem函数来得到真正的对象才能设值。这是因为在laws of reflection中有这么一句话:
Just keep in mind that reflection Values need the address of something in order to modify what they represent.
总结
reflect保重常用的方法克功能基本都涉及到了,不过并没有事无巨细的讲解,还有channel,slice,map,embeded field等没涉及到,更详细的内容只有一边在实际中去探索,一边参看引用,才会更有意义。希望元编程能够在适当的地方改善我们的生活。
reference
- golang reflect package document.
- laws of reflection.(注:网上有有中文翻译版本。)