Go

Go语言的反射

reflect的理解

Posted by BY on January 6, 2018

reflect.Type和reflect.Value

反射功能是由reflect包提供的,它定义了两个重要类型:TypeValue

  • 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.Valueinterface{}都可以包含任意的值。二者的区别是空接口(interface{})隐藏了值的布局信息、内置操作和相关方法,所以除非能知道它的动态类型,并用一个类型断言来渗透进去,否则对所包含值能做的事情很少。对比而言,Value有很多方法可以用来分析所包含的值,而无需知道它的类型。

可以写一个通用格式化函数,称之为format.Any。用reflect.ValueKind方法来区分不同的类型。对类型的分类,分为:

  1. 基础类型:BoolString以及各种数字类型。
  2. 聚合类型:ArrayStruct
  3. 引用类型:Chan,Func,Ptr,Slice,Map,接口类型interface
  4. 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。字段列表包括了从匿名字段中做了提升的字段。

  • mapMapKeys方法返回一个元素类型为reflect.Valueslice,每个元素都是一个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()获取变量需要三步:

  1. 调用Addr(),返回一个Value,其中包含一个指向变量的指针。
  2. 在这个Value上调用Interface(),会返回一个包含这个指针的Interface{}值。
  3. 如果知道变量的类型,可以使用类型断言来把接口内容转换为一个普通指针。之后就可以通过这个指针来更新变量:
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变种:SetIntSetUintSetStringSetFloat等。

x := 2
d := reflect.ValueOf(&x).Elem()  //  d代表变量x
d.SetInt(7)
fmt.Println(x)  //  7

访问结构体字段标签

在一个Web服务器中,大部分HTTP处理函数的第一件事就是提取请求参数到局部变量中。这里将定义一个工具函数Unpack,使用结构体字段标签来简化HTTP处理程序的编写。

首先展示如何使用这个方法。下面的search函数就是一个HTTP处理函数。它定义一个变量datadata的类型是一个字段与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需要做三件事情:

  1. 调用req.ParseForm()来解析请求。在这之后,req.Form就有了所有的请求参数。

  2. Unpack函数构造了一个从每个有效字段名到对应字段的映射。在字段有标签时有效字段名与实际字段名可能会有差别。reflect.TypeField方法会返回一个reflect.StructField类型,这个类型提供了每个字段的名称、类型以及一个可选的标签、它的Tag字段类型为reflect.StructTag,底层类型为字符串,提供了一个Get方法用于解析和提供对于一个特定键的子串。

  3. 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.Typereflect.Value都有一个叫Method的方法。每个从reflect.Type调用的t.Method(i)都会返回一个reflect.Method类型的实例,这个结构类型描述了这个方法的名称和类型。而每个v.Method(i)都会返回一个reflect.Value,代表一个方法值,即一个已绑定接收者的方法。使用reflect.Value.Call方法可以调用Func类型的Value