前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 反射机制详解及实例 【Go语言圣经笔记】

Go 反射机制详解及实例 【Go语言圣经笔记】

作者头像
Steve Wang
发布2021-12-06 16:29:06
1K0
发布2021-12-06 16:29:06
举报
文章被收录于专栏:从流域到海域从流域到海域

反射

Go语言提供了一种机制,能够在运行时更新变量检查它们的值调用它们的方法和它们支持的内在操作而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射(这里反射的定义和其他语言大体相同)。反射也可以让我们将类型本身作为第一类的值类型处理

笔者补充: 第一类值不一定是指面向对象程序设计中所指的对象,而是指程序中的所有实体(比如:变量、函数、队列、字典等等)。一般第一类值具有一下特征:

  • 可以被存入变量或其他结构
  • 可以被作为参数传递给其他方法/函数
  • 可以被作为方法/函数的返回值
  • 可以在执行期被创建,而无需在设计期全部写出
  • 有固定身份

“固有身份”是指实体有内部表示,而不是根据名字来识别,比如匿名函数,还可以通过赋值叫任何名字。大部分语言的基本类型的数值(int, float)等都是第一类值;但是数组不一定,比如C中的数组,作为函数参数时,传递的是第一个元素的地址,同时还丢失了数组长度信息。对于大多数的动态语言,函数/方法都是第一类值,比如Python、Go,但是Ruby不是,因为不能返回一个方法。第一类函数对函数式编程语言来说是必须的。

在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何使用反射机制的:一个是fmt包提供的字符串格式化功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然而,反射是一个复杂的内省技术,不应该随意使用。因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口

为何需要反射?

有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式(类型不确定可能是多个类型中的其中一个),或者是在我们设计该函数的时候这些类型可能还不存在。、

一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用来对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。

我们首先用switch类型分支来测试输入参数是否实现了String方法,如果是的话就调用该方法。然后继续增加类型测试分支,检查这个值的动态类型是否是string、int、bool等基础类型,并在每种情况下执行相应的格式化操作。

代码语言:javascript
复制
func Sprint(x interface{}) string {  // 笔者注:Go语言中 interface{} 可以当泛型用
    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这样的具名类型呢?即使类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的依赖。

没有办法来检查未知类型的表示方式,我们止步于此。这就是我们为何需要反射的原因。

reflect.Type和reflect.Value

Go语言中反射机制的实现是由reflect包提供的。它定义了两个重要的类型, Type和Value。一个Type表示一个Go类型,它是一个接口。有许多方法来区分类型以及检查它们的组成部分, 例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type实现的是接口的类型描述信息(§7.5), 也正是这个实体标识了接口值的动态类型

函数reflect.TypeOf接受任意的interface{}类型, 并以reflect.Type形式返回其动态类型:

代码语言:javascript
复制
t := reflect.TypeOf(3)   // a reflect.Type variable variable为笔者补充
fmt.Println(t.String())  // int
fmt.Println(t)          // int

其中 TypeOf(3)调用将值3(实参)传给interface{}参数(形参). 回到7.5节的将一个具体的值转为接口类型会有一个隐式的接口转换操作该操作会创建一个包含两个信息的接口值: 操作数的动态类型(这里是int)和它的动态的值(这里是3).

因为reflect.TypeOf返回的是一个动态类型的接口值, 它总是返回具体的类型。因此, 下面的代码将打印"*os.File"而不是"io.Writer"(*os.File是声明类型,io.Writer是实际类型)。稍后, 我们将看到能够表达接口类型的 reflect.Type。

代码语言:javascript
复制
var w io.Writer = os.Stdout
fmt.Println(reflect.Typeof(w))  // *os.File 是声明类型 io.Writer是底层类型

要注意的是reflect.Type接口是满足fmt.Stringer接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的,fmt.Printf提供了一个缩写%T参数, 内部使用reflect.TypeOf来输出:

代码语言:javascript
复制
fmt.Printf("%T\n", 3)  // int 等价于输出reflect.Typeof(3) + "\n"

reflect包中另一个重要的类型是Value。 一个reflect.Value可以装载任意类型的值函数reflect.ValueOf接受任意的interface{}类型, 并返回一个装载着其动态值的reflect.Value和reflect.TypeOf类似,reflect.ValueOf返回的结果(的类型)也是具体的类型, 但是reflect.Value也可以持有一个接口值

代码语言:javascript
复制
v := reflect.ValueOf(3)  // a reflect.Value
fmt.Println(v)           // 3
fmt.Println("%v\n", v)    // 3
fmt.Println(v.String())   // note: "<int Value>" 注意这点,v.String()返回接口类型

和reflect.Type类似, reflect.Value也满足fmt.Stringer接口,但是除非Value持有的是字符串,否则String方法只返回其类型。而使用fmt包的%v标志参数会对reflect.Values特殊处理

对Value调用Type方法将返回具体类型所对应的reflect.Type

代码语言:javascript
复制
t := v.Type()  // a reflect.Type variable variable为笔者补充
fmt.Println(t.String())  // int

reflect.ValueOf的逆操作是reflect.Value.Interface方法,它返回一个 interface{}类型,装载着与reflect.Value相同的具体值

代码语言:javascript
复制
v := reflect.ValueOf(3) // a reflect.Value variable varable为笔者补充
x := v.Interface()      // an interface{} variable variable为笔者补充
i := x.(int)            // an int variable variable为笔者补充 另外 interface{} 可以被断言为任何类型
fmt.Printf("%d\n", i)   // "3"

reflect.Value和interface{}都能装载任意的值。有所不同的是, 一个空的接口隐藏了值内部的表示方式和所有方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样), 否则内部值我们没法访问。相比之下,一个Value则有很多方法来检查其内容,,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。

我们使用reflect.Value的Kind方法来替代之前的类型switch。虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的:Bool,String和所有数字类型的基础类型;Array和 Struct对应的聚合类型;Chan,Func,Ptr,Slice,和Map对应的引用类型;interface 类型;还有表示空值的Invalid类型。 (空的reflect.Value的kind即为Invalid。)

代码语言:javascript
复制
// gopl.io/ch12/format

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,
        refelct.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。对于聚合类型(结构体和数组)和接口,只是打印值的类型, 对于引用类型(channels, functions, pointers, slices, 和 maps),打印类型和十六进制的引用地址。虽然还不够理想,但是依然是一个重大的进步,并且Kind只关心底层表示,format.Any也支持具名类型。例如:

代码语言:javascript
复制
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,打印这个值对应的完整结构,同时标记每个元素的发现路径。

让我们从下面的一个示例开始:

代码语言:javascript
复制
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)

在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:

代码语言:javascript
复制
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.y.type = eval.Var
e.args[0].value.y.value = "pi"

你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:

代码语言:javascript
复制
// gopl.io/ch12/display

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

在display函数中,我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数,但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中,path字符串,从最开始传入的起始值(这里是“e”),将逐步增长来表示是如何达到当前值(例如“e.args[0].value”)的。

因为我们不再模拟fmt.Sprint函数,我们将直接使用fmt包来简化我们的例子实现。

代码语言:javascript
复制
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.Spritf("%s[%d]", path, i), v.Index(i))
        }
    case reflect.Struct:
        for i:=0; i<v.NumField(); i++ {
            filedPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name) 
            display(fieldPath, v.Filed(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;如果索引i超出范围的话将导致panic异常,这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理,我们通过在递归处理时向path附加“[i]”来表示访问路径

虽然reflect.Value类型带有很多方法,但是只有少数的方法能对任意值都安全调用。例如,Index方法只能对Slice、数组或字符串类型的值调用,如果对其它类型调用则会导致panic异常。

结构体:NumField方法报告结构体中成员的数量,Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径,我们必须获得结构体对应的reflect.Type类型信息,然后访问结构体第i个成员的名字

Maps: MapKeys方法返回一个reflect.Value类型的slice,每一个元素对应map的一个key。和往常一样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。(我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型;数组、结构体和接口都可以作为map的key。)

指针:Elem方法返回指针指向的变量,依然是reflect.Value类型。即使指针是nil,这个操作也是安全的,在这种情况下指针是Invalid类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。

接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值

现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:

代码语言:javascript
复制
type Movie struct {
    Title, Subtitle string
    Year            int
    Color           bool
    Actor           map[string]string
    Oscars          []string
    Sequel          *string
}

让我们声明一个该类型的变量,然后看看Display函数如何显示它:

代码语言:javascript
复制
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)调用将显示(strangelove电影对应的中文名是《奇爱博士》):

代码语言:javascript
复制
Display strangelove (display.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
strangelove.Oscars[0] = "Best Actor (Nomin.)"
strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
strangelove.Oscars[2] = "Best Director (Nomin.)"
strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil

我们也可以使用Display函数来显示标准库中类型的内部结构,例如*os.File类型:

代码语言:javascript
复制
Display("os.Stderr", os.Stderr)

结果如下:

代码语言:javascript
复制
Output:
Display os.Stderr (*os.File):
(*(*os.Stderr).file).fd = 2
(*(*os.Stderr).file).name = "/dev/stderr"
(*(*os.Stderr).file).nepipe = 0

可以看出,反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的,并且随着标准库的发展也可能导致结果不同。(这也是将这些成员定义为私有成员的原因之一!)

我们甚至可以用Display函数来显示reflect.Value的内部构造(在这里设置为*os.File的类型描述体)。Display(“rV”, reflect.ValueOf(os.Stderr))调用的输出如下,当然不同环境得到的结果可能有差异:

代码语言:javascript
复制
Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = "*os.File"

(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
...
代码语言:javascript
复制
var i interface{} = 3

Display("i", i)

结果如下:

代码语言:javascript
复制
Output:
Display i (int):
i = 3

但如果改成:

代码语言:javascript
复制
Display("&i", &i)

则结果如下

代码语言:javascript
复制
Output:
Display &i (*interface {}):
(*&i).type = int
(*&i).value = 3

在第一个例子中,Display函数调用reflect.ValueOf(i)(笔者注:formatAtom函数调用的),它返回一个Int类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个具体类型的Value,因为它是从一个接口值提取的内容

在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,对这个值调用Elem方法,返回一个Value来表示变量i本身,对应Interface类型。像这样一个间接获得的Value,可能代表任意类型的值,包括接口类型。display函数递归调用自身,这次它分别打印了这个接口的动态类型和值

对于目前的实现,如果遇到对象图中含有回环,Display将会陷入死循环(笔者注:因为递归调用,而且没有明确的终止条件),例如下面这个首尾相连的链表:

代码语言:javascript
复制
// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }  // 笔者注:你可以把Cycle替换成ListNode这样比较好理解
var c Cycle
c = Cycle{42, &c}  // 笔者注:&c 是一个指向自身的指针
DisPlay("c", c)

Display会永远不停地进行深度递归打印:

代码语言:javascript
复制
Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
...ad infinitum...

许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧,需要额外记录迄今访问的路径相应会带来一些成本。通用的解决方案是采用unsafe的语言特性,我们将在13.3节看到具体的解决方案。

带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住,但是这种情况很罕见,不值得付出为了处理回环所需的开销。

示例: 编码为S表达式

Display是一个用于显示结构化数据的调试工具,但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信

正如我们在4.5节中中看到的,Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式采用Lisp语言的语法。但是和其他编码格式不同的是,Go语言自带的标准库并不支持S表达式,主要是因为它没有一个公认的标准规范。

在本节中,我们将定义一个包用于将任意的Go语言对象编码为S表达式格式,它支持以下结构:

代码语言:javascript
复制
42          integer
"hello"     string  (带有Go风格的引号)
foo         symbol (未用引号括起来的名字)
(1 2 3)     list

布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。

我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。

结构体被编码为成员对象的列表,每个成员对象对应一个有两个元素的子列表,子列表的第一个元素是成员的名字,第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。

编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似:

代码语言:javascript
复制
// gopl.io/ch12/sexpr

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.Int())
        
    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().Filed(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(')')  // 笔者注:buf.WriteByte('(')和buf.WriteByte(')')总是成对出现 别遗漏 因为括号是成对出现的 代码模拟了这个过程
    default:
        return fmt.Errof("unsupport type: %s", v.Type()) 
    }
    return nil
}

Marshal函数是对encode的包装,以保持和encoding/…下其它包有着相似的API:

代码语言:javascript
复制
// 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 {  // 笔者注:递归调用中任何一层出现err 都会逐层回溯
        return nil, err
    }
    return buf.Bytes(), nil
}

下面是Marshal对12.3节的strangelove变量编码后的结果:

代码语言:javascript
复制
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
omin.)" "Best Picture (Nomin.)")) (Sequel nil))

整个输出编码为一行中以减少输出的大小,但是也很难阅读。下面是对S表达式手动格式化的结果。编写一个S表达式的美化格式化函数将作为一个具有挑战性的练习任务;不过 http://gopl.io 也提供了一个简单的版本。

代码语言:javascript
复制
((Title "Dr. Strangelove")
 (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
 (Year 1964)
 (Actor (("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")
         ("Dr. Strangelove" "Peter Sellers")))
 (Oscars ("Best Actor (Nomin.)"
          "Best Adapted Screenplay (Nomin.)"
          "Best Director (Nomin.)"
          "Best Picture (Nomin.)"))
 (Sequel nil))

和fmt.Print、json.Marshal、Display函数类似,sexpr.Marshal函数处理带环的数据结构也会陷入死循环(笔者注:因为它们本质上都是靠递归实现,都没有明确的递归终止条件)。

在12.6节中,我们将给出S表达式解码器的实现步骤,但是在那之前,我们还需要先了解如何通过反射技术来更新程序的变量。

通过reflect.Value修改值

到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量

回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x+1和f(2)则不是变量(是表达式,计算结果将返回一个值)。(广义上讲)一个变量,(在实现上)就是一个可寻址的内存空间里面存储了一个值并且其存储的值可以通过内存地址来更新

对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:

代码语言:javascript
复制
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(x)

其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上**,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的**。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value

我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址

代码语言:javascript
复制
fmt.Println(a.CanAddr())    // false
fmt.Println(b.CanAddr())    // false
fmt.Println(c.CanAddr())    // false
fmt.Println(d.CanAddr())    // true

每当我们通过指针间接地获取的reflect.Value都是可取地址的即使开始的是一个不可取地址的Value在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice的索引表达式e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。以此类推,reflect.ValueOf(e).Index(i)对应的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也没有关系。

(笔者注:单纯的值是不可取地址的,它是从其他变量的值拷贝过来的,必须有一个变量对应的值才是可取地址的。可以利用反射机制或者聚合数据类型的索引来取地址)

要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤:

  1. 第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。
  2. 然后是在Value上调用Interface()方法,也就是返回一个interface{},里面包含指向变量的指针。3. 最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了。

示例如下:

代码语言:javascript
复制
x := 2
d := reflect.ValueOf(&x).Elem()     // d refers to the variable 笔者注:但它是个reflect.Value值
px := d.Addr().Interface().(*int)   // px := &x
*px = 3                             // x = 3
fmt.Println(x)                      // 3

或者,不使用指针,而是通过调用可取地址的reflect.Value.Set方法来更新对应的值:

代码语言:javascript
复制
d.Set(reflect.ValueOf(4))  // 笔者注:Set接收的也是reflect.Value值
fmt.Println(x)  // 4

Set方法将在编译时和运行时执行进行类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值:

代码语言:javascript
复制
d.Set(reflect.ValueOf(int64(5)))

同样,对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:

代码语言:javascript
复制
x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3))   // panic: Set using unadressable value

这个地方有很多专用于基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等。

代码语言:javascript
复制
d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x)  // 3

从某种程度上说,这些Set方法总是尽可能地完成任务以SetInt为例,只要变量是某种类型的有符号整数就可以工作,即使是一些命名的类型、甚至只要底层数据类型是有符号整数就可以,而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是:对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使那个interface{}变量对于整数类型也不行。

代码语言:javascript
复制
x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2)                      // OK, x=2
rx.Set(reflect.ValueOf(3))        // OK, x=3 
rx.Setstring("hello")             // pannic: 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结构时,我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,比如在类Unix系统上os.File结构体中的fd int成员。然而,利用反射机制并不能修改这些未导出的成员

代码语言:javascript
复制
stdout := reflect.ValueOf(os.Stdout).Elem()  // *os.Stdout, an os.File var
fmt.Println(stdout.Type())                   // os.File
fd := stdout.FiledByName("fd")
fmt.Println(fd.Int())                        // 1
fd.SetInt(2)                                 // panic: unexported field

一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。因此,CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的:

代码语言:javascript
复制
fmt.Println(fd.CanAddr(), fd.CanSet())  // true false

笔者补充:reflect.ValueOf()和Value.Elem()有什么区别?

补充这个例子也是为了帮助读者深入理解前述内容。 参考:https://www.it1352.com/2102902.html

reflect.ValueOf()返回一个新的Value,该Value被初始化为存储在接口中的具体值。特别的,reflect.ValueOf(nil)返回零值。

Value.Elem()可以对接口或者指针调用。如果对接口调用,返回接口包含的值;如果对指针调用,则返回指针指向的值。如果对非接口也非指针的类型调用,则会产生panic。同样,Value.Elem(nil)返回nil。

笔者注:alue.Elem()返回的值包含了类型信息,可以使用Value.Elem().Type()得到值对应的类型。

reflect.ValueOf()是一个函数,这个函数可以被看作是反射的一个入口。当我们有一个不具备反射功能的普通值,比如int或string,我们可以使用reflect.ValueOf()来得到一个普通值对应的描述符(类型为reflect.ValueOf值),该描述符具备反射功能。

Value.Elem()是reflect.Value类型值的一个方法。因此只能在已经有一个reflect.Value值的前提下使用。可以使用Value.Elem()来得到reflect.Value值指向的值(上面的描述已经说明了reflect.Value是一个描述符,该描述符对应一个原始值,Value.Elem得到这个原始值)。注意到reflect.Indirect()也可以实现这个目的。Value.Elem()还有另外一种比较罕见的高阶用法,将会在文章末尾进行介绍。

如果希望保留原值的反射功能,可以使用常规的Value.Interface()方法,它以interface{}形式返回包装后的值。示例如下:

代码语言:javascript
复制
var i int = 3
var p *int = &i
fmt.Println(p, i)  // 0x414020 3

v := reflect.ValueOf(p)
fmt.Println(v.Interface())  // 0x414020 结果还是指针

v2 := v.Elem()
fmt.Println(v2.Interface())  // 3 结果是值

reflect.Elem()更进一步

reflect.Elem返回接口包含的值或者指针指向的值。

因此,如果reflect.Value包装的是接口值,则Value.Elem()也可以用于获取包装在该接口值中的具体值。

基本上,无论传递给reflect.ValueOf()的值是什么,都会被转化为接口值。如果它还不是接口值,它将被隐式包装在interface{}中。如果传递的值已经是接口值,则存储在其中的具体值将作为interface{}传递。如果传递的是指针,则该指针同样会被包装在interface{}值中。仍然可以使用Value.Elem()获取指向的值,该值将是接口值(而不是具体值),并且再次使用Value.Elem()可以得到具体值。

代码语言:javascript
复制
var r io.Reader = os.Stdin  // os.Stdin是一个实现了io.Reader接口的*os.File类型
v := reflect.ValueOf(r)     // r是一个包装了*os.File的接口值
fmt.Println(v.Type())       // *os.File

v2 := reflect.Valueof(&r)             // 注意:传递的是r的指针,它将会被包装成接口值
fmt.Println(v2.Type())                // Value保存了类型信息,即:*io.Reader
fmt.Println(v2.Elem().Type())         // 取地址对应的值,即:io.Reader类型的接口值 
fmt.Println(v2.Elem().Elem().Type())  // 两次使用Elem,第二次得到了接口值的具体值,即:*os.File

可能有点绕,看不懂的话先回顾一下接口值的概念: 概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。

示例: 解码S表达式

标准库中encoding/…下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的byte slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:

代码语言:javascript
复制
data := []byte{/* ... */}  // 未写明数据内容
var movie Movie
err := json.Unmarshal(data, &movie)

Unmarshal函数使用了反射机制类修改movie变量的每个成员,根据输入的内容为Movie成员创建对应的map、结构体和slice。

现在让我们为S表达式编码实现一个简易的Unmarshal,类似于前面的json.Unmarshal标准库函数,对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须在这里提醒一下,一个健壮的和通用的实现通常需要比下面的实例有更多的代码,例子为了便于演示采用了比较精简的实现版本。我们只支持S表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的只是为了演示反射的用法,而不是构造一个实用的S表达式的解码器。

词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标识符。输入扫描器scanner的Scan方法将提前扫描和返回下一个标识符,对于rune类型。大多数标识符,比如“(”,对应一个单一rune可表示的Unicode字符,但是text/scanner也可以用小的负数表示标识符、字符串等由多个字符组成的标识符。调用Scan方法将返回这些标识符的类型,接着调用TokenText方法将返回记号对应的文本内容。

因为每个解析器可能需要多次使用当前的记号,但是Scan会一直向前扫描,所以我们包装了一个名为lexer的扫描器辅助类型,用于跟踪最近由Scan方法返回的标识符。

代码语言:javascript
复制
// gopl.io/ch12/sexpr

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

func (lex *lexer) next()        { lex.token = len.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("got %q, want %q", lex.text(), want))    
    }
    lex.next()
}

现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标识符,然后根据S表达式的当前标识符更新可取地址的reflect.Value对应的变量v。

代码语言:javascript
复制
func read(lex *lexer, v reflect.Value) {
    switch lex.token {
    case scanner.Ident:
        // The only valid identifiers are "nil" and struct field names
        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表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”是,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符(笔者注:指nil或者结构体成员名),我们都作为错误处理。后面的readList函数将处理结构体的成员名。

最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。

在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。

代码语言:javascript
复制
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()  // 笔者注:注意两次调用了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 file name", lex.text()))
            }
            name := lex.text()
            lex.next()
            read(lex, v.FieldByName(name))
            lex.comsume(')')
        }
        
    case reflect.Map:  // ((key value) ...)
        v.Set(reflect.MakeMap(v.Type()))  // 笔者注:设置一个对应类型的map
        for !endList(lex) {
            lex.consume('(')
            key := reflect.New(v.Type().Key()).Elem() // 创建一个新的key
            read(lex, key)
            value := reflect.New(v.Type().Elem()).Elem() // 创建一个新的value
            read(lex, value)
            v.SetMapIndex(key, value)  // 填充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函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。

代码语言:javascript
复制
// 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()
    defer func() {
        // note: this is not an example of ideal error handing
        if x:=recover(); x!=nil {
            err = fmt.Errof("error at %s: %v", lex.scan.Position, x)
        }
    }()
    read(lex, reflect.ValueOf(out).Elem())
    return nil
}

生产环境下的实现不应该对任何输入问题都以panic形式报告而应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。

获取结构体字段标签

在4.5节我们使用构体成员标签用于设置对应JSON对应的名字。其中json成员标签让我们可以选择成员的名字和抑制零值成员的输出。在本节,我们将看到如何通过反射机制类获取成员标签。

对于一个web服务,大部分HTTP处理函数要做的第一件事情就是展开请求中的参数到本地变量中。我们定义了一个工具函数,叫params.Unpack,通过使用结构体成员标签机制来让HTTP处理函数解析请求参数更方便。

首先,我们看看如何使用它。下面的search函数是一个HTTP请求处理函数。它定义了一个匿名结构体类型的变量,用结构体的每个成员表示HTTP请求的参数。其中结构体成员标签指明了对于请求参数的名字,为了减少URL的长度这些参数名通常都是神秘的缩略词。Unpack将请求参数填充到合适的结构体成员中,这样我们可以方便地通过合适的类型类来访问这些参数。

代码语言:javascript
复制
// gopl.io/ch12/search

import "gopl.io/ch12/parms"

// search implements the /search URL endpoint.
func search(resp http.ResponserWriter, req *http.Request) {
    var data struct {
        Labels      []string `http:"l"`
        MaxResults  int      `http:max`
        Exact       bool     `http:x`
    }
    data.MaxResult = 10  // set default
    if err := params.Upack(req, &data); err != nil {
        http.Error(resp, err.Error(), http.StatusBadRequest)
    }
    
    // ... rest of handler ...
    fmt.Fprintf(resp, "Search: %+v\n", data)
}

笔者注:"Search: %+v\n"表示保留结构体字段名输出值,类似的用法还有#v,不仅保留结构体字段名,还会在字段值上加双引号。

下一步,Unpack函数将构建每个结构体成员有效参数名字到成员变量的映射。如果结构体成员有成员标签的话,有效参数名字可能和实际的成员名字不相同。reflect.Type的Field方法将返回一个reflect.StructField,里面含有每个成员的名字、类型和可选的成员标签等信息。其中成员标签信息对应reflect.StructTag类型的字符串,并且提供了Get方法用于解析和根据特定key提取的子串,例如这里的http:"…"形式的子串。

代码语言:javascript
复制
// gopl.io/ch12/params

// Unpack populates the fields of the struct pointed to by ptr from the HTTP request parameterss in req.
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }
    
    // Build map of fileds keyed by effective name
    fields := make(map[string]reflect.Value)
    v := relfect.ValueOf(ptr).Elem()  // the struct variable
    for i:=0; i<v.NumField(); i++ {
        fieldInfo := v.Type().Field(i)  // a reflect.StructFiled
        tag := fieldInfo.Tag            // a reflect.StructTag 等价于v.Type().Field(i).Tag 得到的是``里面的内容
        name = tag.Get("http")
        if name == "" {
            name = string.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 !.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.Errof("%s: %v", name, err)
                }
                f.Set(reflect.Append(f, elem))
            } else {
                if err := populate(f, value); err != nil {
                    return fmt.Errof("%s: %v", name, err) 
                }
            }
        }
    }
    return nil
}

最后,Unpack遍历HTTP请求的name/valu参数键值对,并且根据更新相应的结构体成员。回想一下,同一个名字的参数可能出现多次。如果发生这种情况,并且对应的结构体成员是一个slice,那么就将所有的参数添加到slice中。其它情况,对应的成员值将被覆盖,只有最后一次出现的参数值才是起作用的

populate函数小心用请求的字符串类型参数值来填充单一的成员v(或者是slice类型成员中的单一的元素)。目前,它仅支持字符串、有符号整数和布尔型。)

代码语言:javascript
复制
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 nil
        }
        v.SetBool(b)
        
    default:
        return fmt.Errorf("unsupported kind %s", v.Type())    
    }
    return nil
}

如果我们上上面的处理程序添加到一个web服务器,则可以产生以下的会话:

代码语言:javascript
复制
$ 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来打印任意值的类型和枚举它的方法:

代码语言:javascript
复制
// 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的实例,它对应一个用于描述一个方法的名称类型的结构体。每次Method(i)方法调用都返回一个reflect.Value以表示对应的值(§6.4),也就是一个方法绑定的的接收者。使用reflect.Value.Call方法(这里没有演示),将可以调用一个Func类型的Value,但是这个例子中只用到了它的类型。

这是属于time.Duration和*strings.Replacer两个类型的方法:

代码语言:javascript
复制
methods.Print(time.Hour)

打印结果如下:

代码语言:javascript
复制
Output:
type time.Duration
func (time.Duration) Hours() float64
func (time.Duration) Minutes() float64
func (time.Duration) Nanoseconds() int64
func (time.Duration) Seconds() float64
func (time.Duration) String() string
代码语言:javascript
复制
methods.Print(new(strings.Replacer))
代码语言:javascript
复制
Output:
type *strings.Replacer
func (*strings.Replacer) Replace(string) string
func (*strings.Replacer) WriteString(io.Writer, string) (int, error)

几点忠告

虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。

反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三:

  1. 基于反射的代码是比较脆弱的
  2. 即使对应类型提供了文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。
  3. 基于反射的代码通常比正常的代码运行速度慢一到两个数量级。

第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的误用问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。

以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对应值的类型、是否可取地址,还有是否可以被修改等。

避免这种因反射而导致的脆弱性的问题的最好方法,是将所有的反射相关的使用控制在包的内部如果可能的话避免在包的API中直接暴露reflect.Value类型这样可以限制一些非法输入如果无法做到这一点,在每个有风险的操作前进行额外的类型检查。以标准库中的代码为例,当fmt.Printf收到一个非法的操作数时,它并不会抛出panic异常,而是打印相关的错误信息。程序虽然还有BUG,但是会更加容易诊断。

代码语言:javascript
复制
fmt.Printf("%d %s\n", "hello", 42)  // "%d(string=hello) %s(int=42)"

反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息

第二个原因是,即使对应类型提供了文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解因此总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档

第三个原因是,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以当反射能使程序更加清晰的时候可以考虑使用。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-10-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 反射
    • 为何需要反射?
      • reflect.Type和reflect.Value
        • Display,一个递归的值打印器
          • 示例: 编码为S表达式
            • 通过reflect.Value修改值
              • 笔者补充:reflect.ValueOf()和Value.Elem()有什么区别?
                • reflect.Elem()更进一步
              • 示例: 解码S表达式
                • 获取结构体字段标签
                  • 显示一个类型的方法集
                    • 几点忠告
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档