reflect.Type和reflect.Value
反射功能是由reflect包提供的,它定义了两个重要类型:Type和Value。
Type表示Go语言的一个类型,它是一个有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分,比如一个结构的各个字段或者一个函数的各个参数。
func main() {
t := reflect.TypeOf(3) // 一个reflect.Type
fmt.Println(t.String()) // int
fmt.Println(t) // int
}
reflect.TypeOf函数接受任何的interface{}参数。把一个具体值赋给一个接口类型时会发生一个隐式类型转换,转换会生成一个包含两部分内容的接口值:动态类型部分是操作数的类型(int),动态值的部分是操作数的值。
因为reflect.Type满足fmt.Stringer,所以fmt.Printf提供了一个简写方式:%T。
func main() {
fmt.Printf("%T\n", 3) // int
}
Value可以包含一个任意类型的值。reflect.ValueOf函数接受任意的interface{}并将接口的动态值以reflect.Value的形式返回。与reflect.TypeOf类似,reflect.ValueOf的返回值也都是具体值,不过reflect.Value也可以包含一个接口值。
func main() {
v := reflect.ValueOf(3)
fmt.Println(v) // 3
fmt.Printf("%v\n", v) // 3
fmt.Println(v.String()) // <int Value>
}
reflect.ValueOf的逆操作是reflect.Value.Interface方法,返回一个interface{}接口值,与reflect.Value包含同一个具体值。
func main() {
v := reflect.ValueOf(3)
x := v.Interface()
i := x.(int)
fmt.Printf("%d\n", i) // 3
}
reflect.Value和interface{}都可以包含任意的值。二者的区别是空接口(interface{})隐藏了值的布局信息、内置操作和相关方法,所以除非能知道它的动态类型,并用一个类型断言来渗透进去,否则对所包含值能做的事情很少。对比而言,Value有很多方法可以用来分析所包含的值,而无需知道它的类型。
可以写一个通用格式化函数,称之为format.Any。用reflect.Value的Kind方法来区分不同的类型。对类型的分类,分为:
- 基础类型:
Bool,String以及各种数字类型。 - 聚合类型:
Array和Struct。 - 引用类型:
Chan,Func,Ptr,Slice,Map,接口类型interface。 Invalid表示没有任何值的类型(reflect.Value的零值就属于Invalid类型)。
// 把任何值格式化为一个字符串
func Any(value interface{}) string {
return formatAtom(reflect.ValueOf(value))
}
// 格式化一个值且不分析其内部结构
func formatAtom(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return "Invalid"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10)
// 省略了浮点数和复数的分支
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface
return v.Type().String() + " value"
}
}
测试函数如下:
func Test(t *testing.T) {
var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x)) // "1"
fmt.Println(format.Any(d)) // "1"
fmt.Println(format.Any([]int64{x})) // "[]int64 0x8202b87b0"
fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"
}
Display 一个递归的值显示器
下面改善组合类型的显示。不再实现一个fmt.Sprint,而是实现一个称谓Display的调试工具函数,这个函数对给定的任意一个复杂值x,输出这个复杂值的完整结构,并对找到的每个元素标上这个元素的路径。比如说:
func main() {
e, _ := eval.Parse("sqrt(A/pi)")
Display("e",e)
}
输出的结果为:
Display e (eval.call)
e.fn = "sqrt"
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = "A"
e.args[0].value.type = eval.Var
e.args[0].value.value = "pi"
在构建是应尽可能避免在包的API里面暴露反射相关的内容。所以定义一个未导出的函数Display来做递归处理,Display只是一个简单的封装且接受一个interface{}参数:
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T)", name, x)
display(name, reflect.ValueOf(x))
}
在display中,使用之前定义的formatAtom函数来输出基础值(基础类型,函数和通道),使用reflect.Value的一些方法来递归展示复杂类型的每个组成部分。当递归深入时,path字符串(之前用来表示起始值,比如”e”)会增长,以表示如何找到当前值(比如”e.args[0].value”)。
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalid\n", path)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++{
display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++{
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
case reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]", path,
formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
解释:
-
slice与数组:这两个的逻辑是一致的。
Len方法返回slice或者数组中元素的个数,Index(i)会返回第i个元素,返回的元素类型为reflect.Value。这两个方法与内置的len(a)和a[i]序列操作类似。在每个序列元素上递归调用了display函数,只是在路径后边加上了”[i]”。 -
结构体:
NumField方法可以报告结果中的字段数,Field(i)会返回第i个字段,返回的字段类型为reflect.Value。字段列表包括了从匿名字段中做了提升的字段。 -
map:
MapKeys方法返回一个元素类型为reflect.Value的slice,每个元素都是一个map的键。与平常遍历map的结果类似,顺序是不固定的。MapIndex(key)返回key对应的值。 -
指针:
Elem方法返回指针指向的变量,同样也是以reflect.Value类型返回。这个方法在指针是nil时也可以正确处理,只不过返回的结果属于Invalid类型,所以用IsNil显式检测空指针,方便输出一条更适合的消息。 -
接口:再次使用
IsNil来判断接口是否为空,如果是非空,通过v.Elem()来获取动态值,进一步输出其类型和值。
测试:
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
strangelove := Movie{
Title: "Dr. Strangelove",
Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
Year: 1964,
Color: false,
Actor: map[string]string{
"Dr. Strangelove": "Peter Sellers",
"Grp. Capt. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
`Maj. T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Best Actor (Nomin.)",
"Best Adapted Screenplay (Nomin.)",
"Best Director (Nomin.)",
"Best Picture (Nomin.)",
},
}
使用reflect.Value来设置值
对于reflect.Value有一个区分,是否是用来寻址的:
x := 2
a := reflect.ValueOf(4)// 值:4, 类型:int, 变量:no
b := reflect.ValueOf(x) // 值:2, 类型:int, 变量:no
c := reflect.ValueOf(&x) // 值:&x, 类型:*int, 变量:no
d := c.Elem() // 值:2, 类型:int, 变量:yes
此处进行解释:a中的值是不可寻址的,因为它包含的仅仅是整数2的一个副本。b也是这样。c中的值也是不可寻址的,它包含的是指针&x的一个副本。所以简单来说,通过reflect.ValueOf(x)返回的reflect.Value都是不可寻址的。而d通过对c中的指针得来的,所以它是可寻址的。可以通过这个方法,调用reflect.ValueOf(&x).Elem()来获得任意变量x可寻址的value值。
也可以通过变量的CanAddr方法来询问reflect.Value是否可寻址:
fmt.Println(a.CanAddr()) // false
fmt.Println(b.CanAddr()) // false
fmt.Println(c.CanAddr()) // false
fmt.Println(d.CanAddr()) // true
从一个可寻址的reflect.Value()获取变量需要三步:
- 调用
Addr(),返回一个Value,其中包含一个指向变量的指针。 - 在这个
Value上调用Interface(),会返回一个包含这个指针的Interface{}值。 - 如果知道变量的类型,可以使用类型断言来把接口内容转换为一个普通指针。之后就可以通过这个指针来更新变量:
x := 2
d := reflect.ValueOf(&x).Elem() // d代表变量x
px := d.Addr().Interface().(*int) // px := &x
*px = 3 // x = 3
fmt.Println(x) // 3
也可以直接通过可寻址的reflect.Value更新变量,不是通过指针,而是直接调用reflect.Value.Set方法:
x := 2
d := reflect.ValueOf(&x).Elem() // d代表变量x
d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4
使用这个方法时需要注意,上面的变量和值都要是int类型,如果是一个不可寻址的或是变量类型不是int类型时调用该方法,会崩溃。
也还有一些为基本类型特化的Set变种:SetInt,SetUint,SetString,SetFloat等。
x := 2
d := reflect.ValueOf(&x).Elem() // d代表变量x
d.SetInt(7)
fmt.Println(x) // 7
访问结构体字段标签
在一个Web服务器中,大部分HTTP处理函数的第一件事就是提取请求参数到局部变量中。这里将定义一个工具函数Unpack,使用结构体字段标签来简化HTTP处理程序的编写。
首先展示如何使用这个方法。下面的search函数就是一个HTTP处理函数。它定义一个变量data,data的类型是一个字段与HTTP请求参数对应的匿名结构。Unpack函数从请求中提取数据来填充这个结构体,这样做在可以方便访问的同时,还避免了手动转换类型。
// search用于处理 /search URL endpoint.
func search(resp http.ResponseWriter, req *http.Request) {
var data struct {
Labels []string `http:"l"`
MaxResults int `http:"max"`
Exact bool `http:"x"`
}
data.MaxResults = 10 // 设置默认值
if err := Unpack(req, &data); err != nil {
http.Error(resp, err.Error(), http.StatusBadRequest) // 400
return
}
// ...其它处理代码...
fmt.Fprintf(resp, "Search: %+v\n", data)
}
之后,Unpack需要做三件事情:
-
调用
req.ParseForm()来解析请求。在这之后,req.Form就有了所有的请求参数。 -
Unpack函数构造了一个从每个有效字段名到对应字段的映射。在字段有标签时有效字段名与实际字段名可能会有差别。reflect.Type的Field方法会返回一个reflect.StructField类型,这个类型提供了每个字段的名称、类型以及一个可选的标签、它的Tag字段类型为reflect.StructTag,底层类型为字符串,提供了一个Get方法用于解析和提供对于一个特定键的子串。 -
Unpack遍历HTTP参数中所有的键值对,并且更新对应的结构体字段。
func Unpack(req *http.Request, ptr interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
// 创建字段映射表,键为有效名称
fields := make(map[string]reflect.Value)
v := reflect.ValueOf(ptr).Elem() // the struct variable
for i := 0; i < v.NumField(); i++ {
fieldInfo := v.Type().Field(i) // a reflect.StructField
tag := fieldInfo.Tag // a reflect.StructTag
name := tag.Get("http")
if name == "" {
name = strings.ToLower(fieldInfo.Name)
}
fields[name] = v.Field(i)
}
// 对请求中的每个参数更新结构体中对应的字段
for name, values := range req.Form {
f := fields[name]
if !f.IsValid() {
continue // 忽略不能识别的HTTP参数
}
for _, value := range values {
if f.Kind() == reflect.Slice {
elem := reflect.New(f.Type().Elem()).Elem()
if err := populate(elem, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
f.Set(reflect.Append(f, elem))
} else {
if err := populate(f, value); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
}
}
}
return nil
}
populate函数负责从单个HTTP请求参数值填充单个字段v或者slice字段中的单个元素。现在,它仅支持字符串、有符号整数和布尔值。
func populate(v reflect.Value, value string) error {
switch v.Kind() {
case reflect.String:
v.SetString(value)
case reflect.Int:
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.SetInt(i)
case reflect.Bool:
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
v.SetBool(b)
default:
return fmt.Errorf("unsupported kind %s", v.Type())
}
return nil
}
最后是将server处理程序添加到一个Web服务器中。下面就是一个交互过程:
$ go build gopl.io/ch12/search
$ ./search &
$ ./fetch 'http://localhost:12345/search'
Search: {Labels:[] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming'
Search: {Labels:[golang programming] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100'
Search: {Labels:[golang programming] MaxResults:100 Exact:false}
$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming'
Search: {Labels:[golang programming] MaxResults:10 Exact:true}
$ ./fetch 'http://localhost:12345/search?q=hello&x=123'
x: strconv.ParseBool: parsing "123": invalid syntax
$ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
max: strconv.ParseInt: parsing "lots": invalid syntax
显示类型的方法
这个反射示例使用reflect.Type来显示一个任意值的类型并枚举它的方法:
func Print(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
fmt.Printf("type %s\n", t)
for i := 0; i < v.NumMethod(); i++ {
methType := v.Method(i).Type()
fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
strings.TrimPrefix(methType.String(), "func"))
}
}
reflect.Type和reflect.Value都有一个叫Method的方法。每个从reflect.Type调用的t.Method(i)都会返回一个reflect.Method类型的实例,这个结构类型描述了这个方法的名称和类型。而每个v.Method(i)都会返回一个reflect.Value,代表一个方法值,即一个已绑定接收者的方法。使用reflect.Value.Call方法可以调用Func类型的Value。