Go程序设计语言 学习笔记 第十二章 反射

Go提供了一种机制,在编译时不知道类型的情况下,可以在运行时更新变量并查看它们的值,调用它们的方法,并应用其表示形式固有的操作。这种机制称为反射。反射还允许我们将类型本身视为头等值(指在编程语言中,某个数据类型(通常是函数或对象)可以像其他数据类型一样被操作和传递)。

在这一章中,我们将探索Go的反射功能,看看它们如何增强语言的表现力,特别是它们如何对fmt提供的字符串格式化和encoding/json、encoding/xml等包提供的协议编码的实现至关重要。反射也是我们在第4.6节中看到的text/template和html/template包提供的模板机制的重要组成部分。然而,反射很复杂,不适合随意使用,因此尽管这些包是使用反射实现的,但它们并不在自己的API中公开反射。

12.1 为什么使用反射

有时候我们需要编写一个函数,能够统一处理那些不符合通用接口、没有已知表示形式,或者在我们设计函数时甚至都不存在的类型的值。

一个熟悉的例子是在fmt.Fprintf中的格式化逻辑,它可以有用地打印出任何类型的任意值,甚至是用户自定义的类型。让我们试着实现一个类似的函数,利用我们已经了解的知识。为了简单起见,我们的函数将接受一个参数,并将结果作为字符串返回,就像fmt.Sprint一样,所以我们将它称为Sprint。

我们首先使用一个type switch来测试参数是否定义了一个String方法,如果是,则调用该方法。然后,我们添加switch情况,测试值的动态类型是否与每种基本类型(字符串、整数、布尔值等)相匹配,并在每种情况下执行相应的格式化操作。

func Sprint(x interface{}) string {
	type stringer interface {
		String() string
	}
	switch x := x.(type) {
	case stringer:
		return x.String()
	case string:
		return x
	case int:
		return strconv.Itoa(x)
	// ...similar cases for int16, uint32, and so on...
	case bool:
		if x {
			return "true"
		}
		return "false"
	default:
		// array, chan, func, map, pointer, slice, struct
		return "???"
	}
}

那么我们该如何处理其他类型,比如[]float64、map[string][]string等等呢?我们可以添加更多情况,但这种类型的数量是无限的。那么命名类型如url.Values呢?即使type switch有一个针对其底层类型map[string][]string的情况,它也不会匹配url.Values,因为这两种类型并不完全相同,而且type switch无法为每种类型都包括一个情况,比如url.Values,因为那样会让这个库依赖于它的客户端。

如果没有一种方法来检查未知类型的值的表示形式,我们很快就会陷入困境。我们需要的是反射。

12.2 reflect.Type和reflact.Value

反射由reflect包提供。它定义了两个重要的类型,Type和Value。Type代表一个Go类型。它是一个接口,具有许多方法用于区分类型并检查它们的组成部分,比如结构体的字段或函数的参数。reflect.Type的唯一实现是类型描述符(7.5节,即接口类型由两部分组成,动态类型和动态值,动态类型用类型描述符来表示),它与标识接口值的动态类型相同。

reflect.TypeOf函数接受任何interface{}类型,并返回其动态类型作为reflect.Type:

	t := reflect.TypeOf(3)  // t的类型为reflect.Type
	fmt.Println(t.String()) // "int"
	fmt.Println(t)          // "int"

上面的TypeOf(3)调用将值3分配给interface{}参数。回顾一下第7.5节,从一个具体值到一个接口类型的赋值执行了隐式的接口转换,这创建了一个接口值,由两个组件组成:它的动态类型是操作数的类型(int),而它的动态值是操作数的值(3)。因为reflect.TypeOf返回一个接口值的动态类型,它总是返回一个具体的类型。因此,例如,下面的代码会打印"*os.File",而不是"io.Writer"。稍后,我们会看到reflect.Type也能够表示接口类型。

    var w io.Writer = os.Stdout
	fmt.Println(reflect.TypeOf(w)) // "*os.File"

注意,reflect.Type满足fmt.Stringer接口。因为打印接口值的动态类型对于调试和日志记录是有用的,所以fmt.Printf提供了一个快捷方式%T,它内部使用了reflect.TypeOf:

fmt.Printf("%T\n", 3) // "int"

reflect包中另一个重要的类型是Value。一个reflect.Value可以持有任何类型的值。reflect.ValueOf函数接受任何interface{}并返回一个包含接口的动态值的reflect.Value。与reflect.TypeOf类似,reflect.ValueOf的结果始终是具体的,但reflect.Value也可以持有接口值。

	v := reflect.ValueOf(3) // a reflect.Value
	fmt.Println(v)          // "3"
	fmt.Printf("%v\n", v)   // "3"
	fmt.Println(v.String()) // NOTE: "<int Value>"

与reflect.Type类似,reflect.Value也满足fmt.Stringer接口,但除非Value持有一个字符串,否则String方法的结果只会显示类型。相反,使用fmt包的%v格式化动词,它对reflect.Value进行了特殊处理。

调用Value的Type方法会返回它的类型作为一个reflect.Type:

	t := v.Type()                // a reflect.Type
	fmt.Println(t.String())      // "int

与reflect.ValueOf的相反操作是reflect.Value.Interface方法。它返回一个interface{},持有与reflect.Value相同的具体值:

	v := reflect.ValueOf(3) // a reflect.Value
	x := v.Interface()      // an interface{}
	i := x.(int)            // an int
	fmt.Printf("%d\n", i)   // "3"

一个reflect.Value和一个interface{}都可以持有任意值。区别在于,一个空接口隐藏了它所持有的值的表示和内在操作,并且不暴露任何方法,因此除非我们知道它的动态类型并使用类型断言来查看它的内部(就像我们之前做的那样),否则我们几乎无法对其中的值进行任何操作。相比之下,Value有许多方法可以检查它的内容,而不管它的类型是什么。让我们使用它们来尝试编写一个通用的格式化函数,我们将其命名为format.Any。

package format

import (
	"reflect"
	"strconv"
)

// Any formats any value as a string.
func Any(value interface{}) string {
	return formatAtom(reflect.ValueOf(value))
}

// formatAtom formats a value without inspecting its internal structure.
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)
	// ...floating-point and complex cases omitted for brevity...
	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"
	}
}

到目前为止,我们的函数将每个值都视为一个不可分割的整体,没有内部结构,因此我们有了formatAtom。对于聚合类型(如结构体和数组)和接口,它仅打印值的类型(如一个s类型的变量,会打印"s value"),而对于引用类型(如通道、函数、指针、切片和映射),它打印类型和十六进制的引用地址。虽然这还不是最理想的,但仍然是一个重大的改进,而且由于Kind只关注底层表示,format.Any也适用于命名类型。例如:

    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"

12.3 Display:一个递归的值显示器

接下来,我们将看一下如何改进复合类型的显示。与其试图完全复制fmt.Sprint,我们将构建一个名为Display的调试工具函数,给定一个任意复杂的值x,它会打印出该值的完整结构,并用找到该元素的路径对每个元素进行标记。让我们从一个示例开始。

e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)

在上面的调用中,Display的参数是来自第7.9节中表达式求值器的语法树。下面是Display的输出:
在这里插入图片描述
在可能的情况下,你应该避免在包的API中暴露反射。我们将定义一个未导出的函数display来执行递归的真正工作,并导出Display,作为它的一个简单包装器,接受一个interface{}参数。

func Display(name string, x interface{}) {
	fmt.Printf("Display %s (%T):\n", name, x)
	display(name, reflect.ValueOf(x))
}

在display中,我们将使用我们之前定义的formatAtom函数来打印基本类型、函数和通道等基本值,但是我们将使用reflect.Value的方法递归地显示更复杂类型的每个组成部分。随着递归的进行,路径字符串,最初描述起始值(例如 “e”),将被增加以指示我们如何到达当前值(例如"e.args[0].value")。

既然我们不再继续实现fmt.Sprint,我们将使用fmt包来保持我们的示例简短。

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))
	}
}

让我们按顺序讨论各种情况。

切片和数组:它们的逻辑是相同的。Len方法返回切片或数组值的元素数量,Index(i)获取索引为i的元素,也返回一个reflect.Value;如果i越界,它会引发panic。这类似于序列上的内置len(a)和a[i]操作。display函数递归地对序列的每个元素调用自身,并将下标标记"[i]"附加到路径上。

虽然reflect.Value有许多方法,但只有少数方法对于任何给定的值都是安全的。例如,Index方法可以在切片、数组或字符串的值上调用,但对于任何其他类型都会引发panic。

结构体:NumField方法报告结构体中字段的数量,而Field(i)返回第i个字段的值作为一个reflect.Value。字段列表包括从匿名字段(定义结构体类型时只提供类型而不写字段名)提升的字段。要在路径上附加字段名,我们必须获取结构体的reflect.Type并访问其第i个字段的名称。

映射:MapKeys方法返回一个reflect.Values的切片,每个元素对应一个键。通常情况下,在遍历映射时,其顺序是未定义的。MapIndex(key) 返回与键对应的值。我们在路径上附加下标标记"[key]"。

指针:Elem方法返回指针指向的变量,同样作为一个reflect.Value。即使指针值为nil,此操作也是安全的,在这种情况下,结果的类型将是无效的,但我们使用IsNil显式检测空指针,以便可以打印更适当的消息。我们在路径前加上一个"*"并将其括在括号中,以避免歧义。

接口:同样,我们使用IsNil来测试接口是否为nil,如果不是,则使用v.Elem()检索其动态值并打印其类型和值。

现在我们的Display函数已经完成,让我们来投入使用。下面的Movie类型是第4.5节中的一个变体。

type Movie struct {
	Title, Subtitle string
	Year            int
	Color           bool
	Actor           map[string]string
	Oscars          []string
	Sequel          *string
}

让我们声明一个这种类型的值,然后看看Display对它做了什么。

	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.)",
		},
	}

调用Display(“strangelove”, strangelove)打印出:
在这里插入图片描述
我们可以使用Display来显示库类型的内部,比如os.File:
在这里插入图片描述
请注意,即使是未导出的字段也对反射可见。请注意,此示例的特定输出可能因平台而异,并且随着库的演变可能会发生变化(这些字段之所以是私有的,是有原因的!)。我们甚至可以将Display应用于reflect.Value,观察其遍历
os.File的类型描述符的内部字段。调用Display(“rV”, reflect.ValueOf(os.Stderr))的输出如下所示,当然,您的结果可能会有所不同:
在这里插入图片描述
在这里插入图片描述
观察这两个示例之间的区别。
在这里插入图片描述
在第一个示例中,Display调用reflect.ValueOf(i),它返回一个kind为Int的值。正如我们在第12.2节中提到的,reflect.ValueOf总是返回一个具体类型的Value,因为它提取了接口值的内容。

在第二个示例中,Display调用reflect.ValueOf(&i),它返回一个指向i的指针,kind为Ptr。Ptr的switch case对此值调用Elem,它返回一个表示变量i本身的Value,其kind为Interface。这种间接获得的Value可以表示任何值,包括接口。display函数会递归调用自己,这一次,它打印出接口的动态类型和值的分开组成部分。目前实现的情况下,如果在对象图中遇到循环,例如吃自己尾巴的链表,Display不会终止:

    // a struct that points to itself
	type Cycle struct {
		Value int
		Tail  *Cycle
	}
	var c Cycle
	c = Cycle{42, &c}
	Display("c", c)

Display打印出这个不断增长的展开:
在这里插入图片描述
许多Go程序至少包含一些循环数据。使Display对抗这样的循环是棘手的,需要额外的簿记来记录到目前为止已经跟踪的引用集合;而且代价也很高。一个通用的解决方案需要使用语言特性unsafe,正如我们将在第13.3节中看到的那样。

循环对fmt.Sprint来说问题较少,因为它很少尝试打印完整的结构。例如,当它遇到一个指针时,它通过打印指针的数值来打破递归。它可能会因尝试打印包含自身作为元素的切片或映射而陷入困境,但这样的情况很少,不值得为处理循环带来相当大的额外麻烦。

12.4 例子:编码S表达式

Display是一个用于显示结构化数据的调试例程,但它几乎可以编码或序列化任意的Go对象,将其转换为适用于进程间通信的可交换标记。正如我们在第4.5节中所看到的,Go的标准库支持多种格式,包括JSON、XML和ASN.1。另一个仍然广泛使用的表示法是S表达式,即Lisp的语法。与其他表示法不同,S表达式并不受Go标准库支持,最重要的是因为它们没有普遍接受的定义,尽管多次尝试进行标准化,并存在许多实现。

在本节中,我们将定义一个包,使用S表达式表示法对任意的Go对象进行编码,该表示法支持以下结构:
在这里插入图片描述
传统上,布尔值使用符号t表示true,使用空列表()或符号nil表示false,但为了简单起见,我们的实现忽略了它们。它还忽略了通道和函数,因为它们的状态对反射来说是不透明的。它还忽略了实数、复数浮点数和接口。为它们添加支持是练习12.3。

我们将按以下思路来把Go语言的值编码为S表达式。整数和字符串以显而易见的方式编码。Nil值被编码为符号nil。数组和切片使用列表表示法进行编码。

结构体被编码为字段绑定的列表,每个字段绑定都是一个两个元素的列表,第一个元素(一个符号)是字段名,第二个元素是字段值。映射也被编码为键值对的列表,每个对都是一个映射条目的键和值。传统上,S表达式使用单个cons cell(Lisp编程语言中的基本数据结构) (key.value) 来表示键值对,而不是两个元素的列表,但为了简化解码,我们将忽略dotted list notation(Lisp中用于标识列表结构的方法,其中,最后一个元素不是一个列表,而是另一个单独的元素,点形式的列表使用点号来分隔最后一个元素和其它元素,例如,(1 2 3 . 4)表示一个列表,其中前三个元素是1、2和 3,最后一个元素是4)。

编码由一个名为encode的单个递归函数完成,如下所示。它的结构基本上与前面章节中的Display相同:

func encode(buf *bytes.Buffer, v reflect.Value) error {
	switch v.Kind() {
	case reflect.Invalid:
		buf.WriteString("nil")

	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Int64:
		fmt.Fprintf(buf, "%d", v.Int())

	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		fmt.Fprintf(buf, "%d", v.Uint())

	case reflect.String:
		fmt.Fprintf(buf, "%q", v.String())

	case reflect.Ptr:
		return encode(buf, v.Elem())

	case reflect.Array, reflect.Slice: // (value ...)
		buf.WriteByte('(')
		for i := 0; i < v.Len(); i++ {
			if i > 0 {
				buf.WriteByte(' ')
			}
			if err := encode(buf, v.Index(i)); err != nil {
				return err
			}
		}
		buf.WriteByte(')')

	case reflect.Struct: // ((name value) ...)
		buf.WriteByte('(')
		for i := 0; i < v.NumField(); i++ {
			if i > 0 {
				buf.WriteByte(' ')
			}
			fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
			if err := encode(buf, v.Field(i)); err != nil {
				return err
			}
			buf.WriteByte(')')
		}
		buf.WriteByte(')')

	case reflect.Map: // ((key value) ...)
		buf.WriteByte('(')
		for i, key := range v.MapKeys() {
			if i > 0 {
				buf.WriteByte(' ')
			}
			buf.WriteByte('(')
			if err := encode(buf, key); err != nil {
				return err
			}
			buf.WriteByte(' ')
			if err := encode(buf, v.MapIndex(key)); err != nil {
				return err
			}
			buf.WriteByte(')')
		}
		buf.WriteByte(')')

	default: // float, complex, bool, chan, func, interface
		return fmt.Errorf("unsupported type: %s", v.Type())
	}
	return nil
}

Marshal函数将编码器包装在一个与其他encoding/...包类似的API中:

// Marshal encodes a Go value in S-expression form.
func Marshal(v interface{}) ([]byte, error) {
	var buf bytes.Buffer
	if err := encode(&buf, reflect.ValueOf(v)); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

这是将Marshal应用于第12.3节中的strangelove变量后的输出:
在这里插入图片描述
整个输出都显示在一行上,间距很小,使得阅读变得困难。以下是按照S表达式惯例手动格式化的相同输出。编写一个美化S表达式的打印程序留作(具有挑战性的)练习;从gopl.io下载的内容中包含了一个简单版本。
在这里插入图片描述
与fmt.Print、json.Marshal和Display函数一样,如果使用循环引用的数据调用sexpr.Marshal,它也会永远循环。

在第12.6节中,我们将概述相应的S表达式解码函数的实现,但在我们开始之前,我们首先需要了解如何使用反射来更新程序变量。

12.5 使用reflect.Value来设置值

到目前为止,在程序中反射还只用来解析变量值。而本节的重点是如何改变值。

请记住,一些Go表达式像x、x.f[1]和*p表示变量,但另一些像x+1和f(2)则不是。变量是一个可寻址的存储位置,其中包含一个值,通过该地址可以更新其值。

类似的区别也适用于reflect.Values。有些是可寻址的,而另一些则不是。考虑以下声明:

	x := 2                   // value   type   variable?
	a := reflect.ValueOf(2)  // 2       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,即使我们最初从一个不可寻址的Value开始。所有关于可寻址性的常规规则在反射中都有对应。例如,由于切片索引表达式e[i]隐式地跟随一个指针,即使表达式e不可寻址,它也是可寻址的。类似地,reflect.ValueOf(e).Index(i)引用一个变量,因此即使reflect.ValueOf(e)不可寻址,它也是可寻址的。

要从可寻址的reflect.Value中恢复变量,需要三个步骤。首先,我们调用Addr(),它返回一个持有指向该变量的指针的reflect.Value。接下来,我们在这个Value上调用Interface(),它返回一个包含指针的interface{}值。最后,如果我们知道变量的类型,我们可以使用类型断言将接口的内容作为普通指针检索出来。然后,我们可以通过指针更新变量:

	x := 2
	d := reflect.ValueOf(&x).Elem()   // d refers to the variable x
	px := d.Addr().Interface().(*int) // px := &x
	*px = 3                           // x = 3
	fmt.Println(x)                    // "3"

或者,我们可以直接通过调用reflect.Value.Set方法更新由可寻址的reflect.Value引用的变量,而不使用指针:

	x := 2
	d := reflect.ValueOf(&x).Elem() // d refers to the variable x
	d.Set(reflect.ValueOf(4))
	fmt.Println(x) // "4"

Set方法在运行时执行了编译器通常执行的相同的可赋值性检查。在上面的例子中,变量和值都具有int类型,但如果变量是int64类型,程序将会发生panic,因此确保值可赋值给变量的类型至关重要:

d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int

当然,在非可寻址的reflect.Value上调用Set也会引发panic:

	x := 2
	b := reflect.ValueOf(x)
	b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value

有一些针对某组基本类型的变种Set方法:SetInt、SetUint、SetString、SetFloat等等:

	x := 2
	d := reflect.ValueOf(&x).Elem()
	d.SetInt(3)
	fmt.Println(x) // "3"
	
	d.SetInt(int64(5))
	fmt.Println(x) // "5"

在某些方面,这些方法更加宽容。例如,SetInt只要变量的类型是某种有符号整数,甚至是一个具有有符号整数基础类型的命名类型,只要值不是太大,它都会成功,并且如果值过大,它将会被安静地截断以适应。但要小心谨慎:在一个指向interface{}变量的reflect.Value上调用SetInt将会引发panic,尽管调用Set会成功。

	x := 1
	rx := reflect.ValueOf(&x).Elem()
	rx.SetInt(2)                     // OK, x = 2
	rx.Set(reflect.ValueOf(3))       // OK, x = 3
	rx.SetString("hello")            // panic: string is not assignable to int
	rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int

	var y interface{}
	ry := reflect.ValueOf(&y).Elem()
	ry.SetInt(2)                     // panic: SetInt called on interface Value
	ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
	ry.SetString("hello")            // panic: SetString called on interface Value
	ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"

当我们将Display应用于os.Stdout时,我们发现反射可以读取那些根据语言的常规规则是不可访问的未导出结构字段的值,比如在类Unix平台上的os.File结构体的fd int字段。然而,反射无法更新这些值:

	stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
	fmt.Println(stdout.Type())                  // "os.File"
	fd := stdout.FieldByName("fd")
	fmt.Println(fd.Int()) // "1"
	fd.SetInt(2)          // panic: unexported field

可寻址的reflect.Value记录了它是否是通过遍历未导出的结构字段获得的,如果是,则不允许修改。因此,在设置变量之前CanAddr通常不是正确的检查方法。相关的方法CanSet报告了一个reflect.Value是否可寻址且可设置:

fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"

12.6 例子:解码S表达式

对于标准库中的encoding/…包提供的每个Marshal函数,都有一个相应的Unmarshal函数用于解码。例如,正如我们在第4.5节中所看到的,给定一个包含我们的Movie类型(§12.3)的JSON编码数据的字节切片,我们可以这样解码它:

	data := []byte{ /* ... */ }
	var movie Movie
	err := json.Unmarshal(data, &movie)

Unmarshal函数使用反射来修改现有电影变量的字段,根据Movie类型和传入数据的内容创建新的映射、结构体和切片。

现在让我们实现一个简单的Unmarshal函数,用于S表达式,类似于上面使用的标准json.Unmarshal函数,以及我们之前的sexpr.Marshal的反函数。我们必须提醒你,一个健壮和通用的实现需要比本例简单实现的代码多得多,而这已经很长了,所以我们采取了很多捷径。我们只支持了S表达式的有限子集,并且没有很好地处理错误。这段代码旨在说明反射,而不是解析。

词法分析器使用了text/scanner包中的Scanner类型将输入流分解为一系列token,如注释、标识符、字符串字面量和数值字面量。扫描器的Scan方法推进扫描器,并返回下一个token,token的类型为rune。大多数token,比如’(',由单个rune组成,但text/scanner包使用rune类型的小负数区域来表示多字符的token的类型,如Ident(标识符)、String和Int。在调用返回其中一种类型的token的Scan后,扫描器的TokenText方法返回token的文本。

由于典型的解析器可能需要多次检查当前标记,但Scan方法会推进扫描器,因此我们将扫描器封装在一个名为lexer的帮助器类型中,该类型跟踪由Scan最近返回的标记。

type lexer struct {
	scan  scanner.Scanner
	token rune // the current token
}

func (lex *lexer) next()        { lex.token = lex.scan.Scan() }
func (lex *lexer) text() string { return lex.scan.TokenText() }

func (lex *lexer) consume(want rune) {
	if lex.token != want { // NOTE: Not an example of good error handling.
		panic(fmt.Sprintf("get %q, want %q", lex.text(), want))
	}
	lex.next()
}

现在让我们转向解析器。它由两个主要函数组成。第一个函数是read,它读取以当前标记(token)开头的S表达式,并更新由可寻址的reflect.Value v引用的变量。

func read(lex *lexer, v reflect.Value) {
	switch lex.token {
	// scanner通过是否有引号引起、是否满足标识符的条件限制等方式来判断一个串是否是标识符
	case scanner.Ident:
		// The Only valid identifiers are
		// "nil" and struct field names.
		// 如果标识符名是nil,即指针的空值
		if lex.text() == "nil" {
			v.Set(reflect.Zero(v.Type()))
			lex.next()
			return
		}
	case scanner.String:
		s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
		v.SetString(s)
		lex.next()
		return
	case scanner.Int:
		i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
		v.SetInt(int64(i))
		lex.next()
		return
	case '(':
		lex.next()
		readList(lex, v)
		lex.next() // consume ')'
		return
	}
	panic(fmt.Sprintf("unexpected token %q", lex.text()))
}

我们的S表达式使用标识符来表示两种不同的目的,即结构字段名称和指针的空值。read函数只处理后一种情况。当它遇到scanner.Ident "nil"时,它使用reflect.Zero函数将v设置为其类型的零值。对于任何其他标识符,它会报告错误。readList函数,我们马上就会看到,处理用作结构字段名称的标识符。

‘(‘标记表示列表的开始。第二个函数readList将列表解码为复合类型的变量,例如map、struct、slice或array,具体取决于我们当前要填充的Go变量的类型。在每种情况下,循环继续解析项目,直到遇到与之匹配的闭括号’)’,这是由endList函数检测到的。

有趣的部分是递归。最简单的情况是数组。直到看到闭括号’)'为止,我们使用Index获取数组中的每个变量,并进行递归调用read进行填充。在许多其他错误情况下,如果输入数据导致解码器超出数组的末尾索引,解码器将会抛出panic。对于切片,我们使用类似的方法,不同之处在于我们必须为每个元素创建一个新的变量,填充它,然后将其附加到切片中。

对于结构体和映射,循环必须在每次迭代中解析一个(key value)子列表。对于结构体,键是用来定位字段的符号。类似于数组的情况,我们使用FieldByName获取结构体字段的现有值,并进行递归调用以填充它。对于map,键可以是任何类型,类似于切片的情况,我们创建一个新变量,递归填充它,最后将新的键值对插入map中。

func readList(lex *lexer, v reflect.Value) {
	switch v.Kind() {
	case reflect.Array: // (item ...)
		for i := 0; !endList(lex); i++ {
			read(lex, v.Index(i))
		}

	case reflect.Slice: // (item ...)
		for !endList(lex) {
			item := reflect.New(v.Type().Elem()).Elem()
			read(lex, item)
			v.Set(reflect.Append(v, item))
		}

	case reflect.Struct: // ((name value) ...)
		for !endList(lex) {
			lex.consume('(')
			if lex.token != scanner.Ident {
				panic(fmt.Sprintf("got token %q, want field name", lex.text()))
			}
			name := lex.text()
			lex.next()
			read(lex, v.FieldByName(name))
			lex.consume(')')
		}

	case reflect.Map: // ((key value) ...)
		v.Set(reflect.MakeMap(v.Type()))
		for !endList(lex) {
			lex.consume('(')
			key := reflect.New(v.Type().Key()).Elem()
			read(lex, key)
			value := reflect.New(v.Type().Elem()).Elem()
			read(lex, value)
			v.SetMapIndex(key, value)
			lex.consume(')')
		}

	default:
		panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
	}
}

func endList(lex *lexer) bool {
	switch lex.token {
	case scanner.EOF:
		panic("end of file")
	case ')':
		return true
	}
	return false
}

最后,我们将解析器封装在一个名为Unmarshal的导出函数中,如下所示,它隐藏了实现中的一些细节。在解析过程中遇到的错误会导致panic,因此Unmarshal使用了延迟调用来从panic中恢复,并返回一个错误消息。

// Unmarshal parses S-expression data and populates the variable
// whose address is in the non-nil pointer out.
func Unmarshal(data []byte, out interface{}) (err error) {
	lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
	lex.scan.Init(bytes.NewReader(data))
	lex.next() // get the first token
	defer func() {
		// NOTE: this is not an example of ideal error handling.
		if x := recover(); x != nil {
			err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
		}
	}()
	read(lex, reflect.ValueOf(out).Elem())
	return nil
}

一款生产级别的实现在面对任何输入时都不应该出现panic,而应该对每个错误都提供有意义的报告,可能包括行号或偏移量。尽管如此,我们希望这个例子能传达一些关于像encoding/json这样的包内部发生了什么,以及如何利用反射来填充数据结构的想法。

12.7 访问结构体字段标签

在第4.5节中,我们使用了结构字段标签来修改Go结构值的JSON编码。json字段标签允许我们选择替代字段名称以及不输出空字段。在这一节中,我们将看到如何使用反射来访问字段标签。

在一个Web服务器中,大多数HTTP处理函数首先要做的事情是将请求参数提取到本地变量中。我们将定义一个实用函数params.Unpack,它使用结构字段标签来更方便地编写HTTP处理程序(7.7节)。

首先,我们将展示它的用法。下面的search函数是一个HTTP处理程序。它定义了一个名为data的匿名结构类型的变量,其字段对应于HTTP请求参数。结构的字段标签指定了参数名称,这些名称通常很短且晦涩,因为URL中的空间很珍贵。Unpack函数从请求中填充结构,以便参数可以方便地访问,并具有适当的类型。

// search implements the /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 // set default
	if err := params.Unpack(req, &data); err != nil {
		http.Error(resp, err.Error(), http.StatusBadRequest) // 400
		return
	}

	// ...rest of handler...
	// %+v用于格式化输出结构体,会输出结构体的字段名及其对应的值,包括未导出字段的值
	fmt.Fprintf(resp, "Search: %+v\n", data)
}

下面的Unpack函数执行三项任务。首先,它调用req.ParseForm()来解析请求。随后,req.Form包含了所有的参数,无论HTTP客户端是使用GET还是POST请求方法。

接着,Unpack会构建一个从每个字段的有效名称到该字段变量的映射。有效名称可能与实际名称不同,如果字段有标签的话。reflect.Type的Field方法返回一个reflect.StructField,提供关于每个字段的信息,如名称、类型和可选标签。Tag字段是一个reflect.StructTag,它是一个字符串类型,提供了一个Get方法来解析和提取特定键的子字符串,例如上例中的http:"..."

// Unpack populates the fields of the struct point to by ptr
// from the HTTP request paramters in req.
func Unpack(req *http.Request, ptr interface{}) error {
	if err := req.ParseForm(); err != nil {
		return err
	}

	// Build map of fields keyed by effective name.
	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)
	}

	// Update struct field for each parameter in the request.
	for name, values := range req.Form {
		f := fields[name]
		if !f.IsValid() {
			continue // ignore unrecognized HTTP parameters
		}
		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
}

最后,Unpack函数遍历HTTP参数的名称/值对,并更新相应的结构字段。请记住,同一个参数名称可能会出现多次。如果发生这种情况,并且字段是一个切片,则该参数的所有值都会被累积到切片中。否则,该字段会被重复覆盖,以致于只有最后一个值起作用。

populate函数负责从参数值中设置单个字段v(或切片字段的单个元素)。目前,它仅支持字符串、有符号整数和布尔值。支持其他类型留作练习。

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
}

如果我们将服务器处理程序添加到一个Web服务器中,这可能是一个典型的会话:
在这里插入图片描述
12.8 显示类型的方法

最后一个反射示例使用reflect.Type来显示一个任意值的类型并枚举它的方法:

// gopl.io/ch12/methods
// Print prints the method set of the value x.
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的方法。每次t.Method(i)调用都会返回一个reflect.Method实例,它是一个结构类型,描述了单个方法的名称和类型。每次v.Method(i)调用都会返回一个reflect.Value,表示一个方法值(6.4节),即绑定到其接收器的方法。使用reflect.Value.Call方法(我们这里没有展示)可以像这样调用Func类型的值,但是这个程序只需要它的Type。

下面是属于两种类型time.Duration和*strings.Replacer的方法:
在这里插入图片描述
12.9 注意事项

我们没有足够的空间展示反射API的全部内容,但前面的例子给出了一些想法,这些想法关于什么是可能的。反射是一个强大而表达力丰富的工具,但应该谨慎使用,有三个原因。

第一个原因是基于反射的代码可能很脆弱。对于每个导致编译器报告类型错误的错误,都是一种对反射的误用方式,但是编译器在构建时报告这种错误,而反射错误则在执行过程中作为panic报告,可能在程序编写很久甚至开始运行很久之后才会出现。

举个例子,如果readList函数(12.6节)应该从输入中读取一个字符串但填充了一个类型为int的变量,那么调用reflect.Value.SetString就会导致panic。大多数使用反射的程序都有类似的危险,并且需要极大的注意来跟踪每个reflect.Value的类型、可寻址性和可设置性。

避免这种脆弱性的最佳方法是确保反射的使用完全封装在你的包内,并且如果可能的话,避免使用reflect.Value,而是优先使用包API中的特定类型,以限制输入到合法值。如果这不可能,可以在每个风险操作之前执行额外的动态检查。例如,标准库中的fmt.Printf在将格式符应用于不恰当的操作数时并不会神秘地panic,而是打印一个信息丰富的错误消息。虽然程序仍然存在bug,但更容易诊断。

fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"

反射还降低了自动重构和分析工具的安全性和准确性,因为它们无法确定或依赖于类型信息。

避免使用反射的第二个原因是,由于类型充当了一种文档形式,而反射的操作不能受到静态类型检查的约束,因此具有高度反射性的代码通常很难理解。始终要仔细记录接受interface{}或reflect.Value的函数的期望类型和其他不变性。

第三个原因是,基于反射的函数可能比针对特定类型专门化的代码慢一两个数量级。在典型程序中,大多数函数与整体性能无关,因此在使程序更清晰时使用反射是可以接受的。测试特别适合使用反射,因为大多数测试使用小数据集。但是对于关键路径上的函数,最好避免使用反射。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值