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

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 xcase 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, structreturn "???"}
}

那么我们该如何处理其他类型,比如[]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.Typefmt.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.Stdoutfmt.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.Valuefmt.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.Typefmt.Println(t.String())      // "int

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

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

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

package formatimport ("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.Interfacereturn v.Type().String() + " value"}
}

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

    var x int64 = 1var d time.Duration = 1 * time.Nanosecondfmt.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, funcsfmt.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 stringYear            intColor           boolActor           map[string]stringOscars          []stringSequel          *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 itselftype Cycle struct {Value intTail  *Cycle}var c Cyclec = 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, interfacereturn 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.Bufferif 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    nob := reflect.ValueOf(x)  // 2       int    noc := reflect.ValueOf(&x) // &x      *int   nod := 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 := 2d := reflect.ValueOf(&x).Elem()   // d refers to the variable xpx := d.Addr().Interface().(*int) // px := &x*px = 3                           // x = 3fmt.Println(x)                    // "3"

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

	x := 2d := reflect.ValueOf(&x).Elem() // d refers to the variable xd.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 := 2b := reflect.ValueOf(x)b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value

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

	x := 2d := 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 := 1rx := reflect.ValueOf(&x).Elem()rx.SetInt(2)                     // OK, x = 2rx.Set(reflect.ValueOf(3))       // OK, x = 3rx.SetString("hello")            // panic: string is not assignable to intrx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to intvar y interface{}ry := reflect.ValueOf(&y).Elem()ry.SetInt(2)                     // panic: SetInt called on interface Valuery.Set(reflect.ValueOf(3))       // OK, y = int(3)ry.SetString("hello")            // panic: SetString called on interface Valuery.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 varfmt.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 Movieerr := 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.Scannertoken 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 errorsv.SetString(s)lex.next()returncase scanner.Int:i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errorsv.SetInt(int64(i))lex.next()returncase '(':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 tokendefer 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 defaultif err := params.Unpack(req, &data); err != nil {http.Error(resp, err.Error(), http.StatusBadRequest) // 400return}// ...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 variablefor i := 0; i < v.NumField(); i++ {fieldInfo := v.Type().Field(i) // a reflect.StructFieldtag := fieldInfo.Tag           // a reflect.StructTagname := 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的函数的期望类型和其他不变性。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/631532.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【高级RAG技巧】在大模型知识库问答中增强文档分割与表格提取

前言 文档分割是一项具有挑战性的任务&#xff0c;它是任何知识库问答系统的基础。高质量的文档分割结果对于显著提升问答效果至关重要&#xff0c;但是目前大多数开源库的处理能力有限。 这些开源的库或者方法缺点大致可以罗列如下&#xff1a; 只能处理文本&#xff0c;无法…

我与C++的爱恋:日期计算器

​ ​ &#x1f525;个人主页&#xff1a;guoguoqiang. &#x1f525;专栏&#xff1a;我与C的爱恋 朋友们大家好啊&#xff0c;在我们学习了默认成员函数后&#xff0c;我们通过上述内容&#xff0c;来实现一个简易的日期计算器。 ​ ​ 头文件的声明 #pragma once #incl…

Odoo|手把手教你Odoo集成drools,完成物料规则配置与报价单自动审核!

一、背景介绍 在实际业务中&#xff0c;售前根据客户需求选择相应的产品和对应的物料来生成报价单。然而&#xff0c;在填写报价单的过程中&#xff0c;可能会出现物料漏选或数量不准确的情况&#xff0c;这会对后续备货和生产效率造成重大影响。此外&#xff0c;由于产品和物料…

Elasticsearch:简化 KNN 搜索

作者&#xff1a;来自 Elastic Panagiotis Bailis 在这篇博客文章中&#xff0c;我们将深入探讨我们为了使 KNN 搜索的入门体验变得更加简单而做出的努力&#xff01; 向量搜索 向量搜索通过在 Elasticsearch 中引入一种新的专有的 KNN 搜索类型&#xff0c;已经可以使用一段…

Linux 2.进程(return、exit、_exit、atexit注册进程终止处理函数)

return、exit、_exit、atexit注册进程终止处理函数 return、exit、_exitatexit注册进程终止处理函数return、exit和_exit的区别&#xff1a;return()、exit()_exit() return、exit、_exit 正常终止&#xff1a;return、exit、_exit 非正常终止&#xff1a;自己或他人发信号终止…

VMware配置CentOS 7 并实现ssh连接

Vmware 17下载地址 ***永久许可证&#xff1a;***5Y012-8HL8P-0J8U0-032Q6-93KKF CentOS 7 下载地址 一、配置CentOS 如下 创建新的虚拟机&#xff0c;选择典型&#xff0c;点击下一步 选择上述下载镜像存储位置&#xff0c;选择镜像&#xff0c;点击下一步 3.填写相关信息…

SVD奇异值分解与PCA主成分分析

目录 SVD奇异值分解SVD的思想SVD的直观理解SVD的几何含义SVD更严格的数学推导求奇异值 SVD和矩阵特征分解的关系 应用实例数据压缩降噪数据分析坐标系变换 PCA主成分分析直觉分析期望与方差协方差与协方差矩阵主成分分析法降维的思路直接去掉特征&#xff1f;太盲目从解除特征之…

攻防世界fileclude题解

攻防世界fileclude题解 ​​ 题目要求file1和file2参数不能为空 且file2这个文件内容值为hello ctf&#xff0c;用php://input 然后POST体内输入hello ctf即可满足这个if条件 满足这个条件后就会包含file1变量所指定的那个文件。用php伪协议来跨目录包含一下flag.php文件就可以…

[大模型]Qwen-Audio-chat FastApi 部署调用

Qwen-Audio-chat FastApi 部署调用 Qwen-Audio 介绍 Qwen-Audio 是阿里云研发的大规模音频语言模型&#xff08;Large Audio Language Model&#xff09;。Qwen-Audio 可以以多种音频 (包括说话人语音、自然音、音乐、歌声&#xff09;和文本作为输入&#xff0c;并以文本作为…

PM要会项目管理?完整版项目管理经验分享

近9个月&#xff0c;公司发生许多事情&#xff0c;包括产品研发部的人员结构调整。 原本以产品经理负责制的小组研发&#xff0c;变成了以项目经理负责制的项目组研发。 对于这一调整&#xff0c;我是支持的&#xff0c;毕竟产品在跟进项目时对技术的管控能力确实不如懂技术的…

02节-51单片机-LED模块

文章目录 1.点亮一个LED灯2.LED闪烁3.LED流水灯 1.点亮一个LED灯 #include <REGX52.H> void main() {P20xFE; //1111 1110while(1){} }2.LED闪烁 增加延时&#xff0c;控制LED的亮灭间隙 延时函数的添加依靠STC-ISP软件的延时函数功能代码自动生成&#xff0c;如图 #i…

基础拓扑学习

基础拓扑 有限集、可数集和不可数集 2.1 定义 考虑两个集 A A A和 B B B&#xff0c;他们的元素可以是任何东西。假定对于 A A A的每个元素 x x x&#xff0c;按照某种方式&#xff0c;与集 B B B的一个元素联系着&#xff0c;这个元素记作 f ( x ) f\left( x \right) f(x);那…

布偶猫挑食怎么办?试试这款猫粮!有它全搞定

亲爱的朋友们&#xff0c;你是不是也在为自家的小布偶猫挑选一款适口性好的猫粮而犯愁呢&#xff1f;别担心&#xff0c;今天我就来给大家推荐一款我家小宝贝超爱的福派斯鲜肉无谷理想体态去毛球猫粮&#xff01;&#x1f431; 首先&#xff0c;我们要知道&#xff0c;布偶猫这…

C++信奥教学PPT:CSP_J_算法之双指针算法(中)

1、⼀个⻓度为 n-1 的递增排序数组中的所有数字都是唯⼀的&#xff0c;并且每个数字都在范围0&#xff5e;n-1 之内。在范围 0&#xff5e; n-1 内的 n 个数字中有且只有⼀个数字不在该数组中&#xff0c;请找出这个数字。 2、循环最大值&#xff08;Maximum in the Cycle of 1…

Vue3:组合式API的基本使用

一、前言 本文主要讲述在Vue3中组合式API的基本使用。 二、组合式API的写法 1、数据 组合式API中&#xff0c;数据在setup函数里面声明数据要在return中返回才能在模板中渲染使用如果数据类型不是响应式数据&#xff0c;当数据改变时&#xff0c;已经展示在页面上的数据不会…

UnityShader——基础篇之渲染流水线

渲染流水线 综述 什么是渲染流水线 渲染流水线的工作任务在于由一个三维场景出发、生成&#xff08;或者说渲染&#xff09;一张二维图像&#xff0c;换句话说&#xff0c;计算机需要从一系列的顶点数据、纹理等信息出发&#xff0c;把这些信息最终转换成一张人眼可以看到的图…

【话题】程序员如何搞副业,简单探讨下

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读小5的系列文章&#xff0c;这是《话题》系列文章 目录 背景前提条件打造私域广结朋友平台 技能转化为价值1. 副业途径2. 如何开展3. 未来趋势与建议4. 挑战与策略5. 规划与发展 文章推荐 背景 程序员不仅拥有将抽象概念转化…

Linux 操作系统gdb、makefile

今天是对前面两天的补充和完善。 1、gdb 1.1 gdb 作用 调试程序 1.2 调试bug的步骤 测试&#xff1a;发现问题 固化&#xff1a;让bug重现 定位&#xff1a;找到bug的位置 修改&#xff1a;修改bug 验证 1.3 gdb调试工具的使用 1->想要使用gdb调试工具&#xff0c;在编…

OceanBase数据库日常运维快速上手

这里为大家汇总了从租户创建、连接数据库&#xff0c;到数据库的备份、归档、资源配置调整等&#xff0c;在OceanBase数据库日常运维中的操作指南。 创建租户 方法一&#xff1a;通过OCP 创建 确认可分配资源 想要了解具体可分配的内存量&#xff0c;可以通过【资源管理】功…

vue3 -- 项目使用自定义字体font-family

在Vue 3项目中使用自定义字体(font-family)的方法与在普通的HTML/CSS项目中类似。可以按照以下步骤进行操作: 引入字体文件: 首先,确保你的字体文件(通常是.woff、.woff2、.ttf等格式)位于项目中的某个目录下,比如src/assets/font/。 在全局样式中定义字体: 在你的全局…