gopl 反射2

本篇各章节的主要内容: golang

  1. 使用 reflect.Value 来设置值:经过 Elem() 方法获取指针对应的值,而后就能够修改值
  2. 示例,解码 S 表达式:以前内容的综合运用
  3. 访问结构体成员标签:像JSON反序列化那样,使用反射获取成员标签,并填充结构体的字段
  4. 显示类型的方法:经过一个简单的示例,获取任意值的类型,并枚举它的方法,还能够调用这些方法
  5. 注意事项:慎用反射,缘由有三

使用 reflect.Value 来设置值

到目前为止,反射只是用来解析变量值。本节的重点是改变值。 json

可寻址的值(canAddr)

reflect.Value 的值,有些是可寻址的,有些是不可寻址的。经过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可寻址的。可是经过指针提领得来的 reflect.Value 是可寻址的。能够经过调用 reflect.ValueOf(&x).Elem() 来得到任意变量 x 可寻址的 reflect.Value 值。
能够经过变量的 CanAddr 方法来询问 reflect.Value 变量是否可寻址:数组

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)

fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

更新变量(Set)

从一个可寻址的 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 方法:服务器

d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"

注意事项

若是类型不匹配会致使程序崩溃:数据结构

d.Set(reflect.ValueOf(int64(5))) // panic: int64 不可赋值给 int

在一个不可寻址的 reflect.Value 上调用 Set 方法也会使程序崩溃:ide

x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: 在不可寻址的值上使用 Set 方法

另外还提供了一些为基本类型特化的 Set 变种:SetInt、SetUint、SetString、SetFloat等:函数

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

这些方法还有必定的容错性。好比 SetInt 方法,任意有符号整型,甚至是底层类型是有符号整型的命名类型,均可以执行成功。若是值太大了,会无提示地截断它。可是在指向 interface{} 变量的 reflect.Value 上调用 SetInt 会崩溃(尽管使用 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 不能赋值给 int
rx.Set(reflect.ValueOf("hello")) // panic: string 不能赋值给 int

var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2)                     // panic: 在指向空接口的 Value 上调用 SetInt
ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
ry.SetString("hello")            // panic: 在指向空接口的 Value 上调用 SetString
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"

可修改的值(CanSet)

另外,反射能够越过 Go 言语的导出规则,读取到未导出的成员。可是利用反射不能修改未导出的成员:

stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, 一个 os.File 变量
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"

示例:解码 S 表达式

本节要为 S 表达式编码实现一个简单的 Unmarshal 函数(解碼器)。一个健壮的和通用的实现比这里的例子须要更多的代码,这里精简了不少,只支持 S 表达式有限的子集,而且没有优雅地处理错误。代码的目的是阐释反射,而不是语法分析。

词法分析器

词法分析器 lexer 使用 text\/scanner 包提供的扫描器 Scanner 类型来把输入流分解成一系列的标记(token),包括注释、标识符、字符串字面量和数字字面量。扫描器的 Scan 方法将提早扫描并返回下一个标记(类型为 rune)。大部分标记(好比'(')都只包含单个rune,但 text\/scanner 包也能够支持由多个字符组成的记号。调用 Scan 会返回标记的类型,调用 TokenText 则会返回标记的文本。
由于每一个解析器可能须要屡次使用当前的记号,可是 Scan 会一直向前扫描,因此把扫描器封装到一个 lexer 辅助类型中,其中保存了 Scan 最近返回的标记:

type lexer struct {
    scan  scanner.Scanner
    token rune // 当前标记
}

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 { // 注意: 错误处理不是这篇的重点,简单粗暴的处理了
        panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
    }
    lex.next()
}

函数实现

分析器有两个主要的函数。
一个是read,它读取从当前标记开始的 S 表达式,并更新由可寻址的 reflect.Value 类型的变量 v 指向的变量:

func read(lex *lexer, v reflect.Value) {
    switch lex.token {
    case scanner.Ident:
        // 仅有的有标识符是 “nil” 和结构体的字段名
        if lex.text() == "nil" {
            v.Set(reflect.Zero(v.Type()))
            lex.next()
            return
        }
    case scanner.String:
        s, _ := strconv.Unquote(lex.text()) // 注意:错误被忽略
        v.SetString(s)
        lex.next()
        return
    case scanner.Int:
        i, _ := strconv.Atoi(lex.text()) // 注意:错误被忽略
        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 函数只处理后一种状况。当它遇到 scanner.Ident 的值为 “nil” 时,经过 reflect.Zero 函数把 v 设置为其类型的零值。对于其余标识符,则应该产生一个错误(这里则是采用简单粗暴的方法,直接忽略了)。

还有一个是 readList 函数。一个 '(' 标记表明一个列表的开始,readList 函数可把列表解码为多种类型:map、结构体、切片或者数组,具体类型根据传入待填充变量的类型决定。对于每种类型都会循环解析内容直到遇到匹配的右括号 ')',这个是用 endList 函数来检测的。
比较有趣的地方是递归。最简单的例子是处理数组,在遇到 ')' 以前,使用 Index 方法来得到数组的一个元素,再递归调用 read 来填充数据。切片的流程与数组相似,不一样之处是先建立每个元素变量,再填充,最后追加到切片中。
结构体和map在循环的每一轮中都必须解析一个关于(key value)的子列表。对于结构体,key 是用来定位字段的符号。与数组相似,经过 FieldByName 函数来得到结构体对应字段的变量,再递归调用 read 来填充。对于 map,key 能够是任何类型。与切片相似,先建立新变量,再递归地填充,最后再把新的键值对添加到 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,隐藏了实现中多个不完美的地方,好比解析过程当中遇到错误会崩溃,所以使用了一个延迟调用来从崩溃中恢复,而且返回错误消息:

// Unmarshal 解析 S 表达式数据而且填充到非 nil 指针 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() {
        // 注意: 错误处理不是这篇的重点,简单粗暴的处理了
        if x := recover(); x != nil {
            err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
        }
    }()
    read(lex, reflect.ValueOf(out).Elem())
    return nil
}

一个具有用于生产环境的质量的实现对任何的输入都不该当崩溃,并且应当对每次错误详细报告信息,可能的话,应当包含行号或者偏移量。经过这个示例有助于了解 encoding/json 这类包的底层机制,以及如何使用反射来填充数据结构。

访问结构体字段标签

这里的“成员”和“字段”两个词有点混用,但都是同一个意思。
可使用结构体成员标签(field tag)在进行JSON反序列化的时候对应JSON中字段的名字。json 成员标签让咱们能够选择其余的字段名以及忽略输出的空字段。这小节将经过反射机制获取结构体字段的标签,而后填充字段的值,就和JSON反序列化同样,目标和结果是同样的,只是获取的数据源不一样。
有一个 Web 服务应用的场景,在 Web 服务器中,绝大部分 HTTP 处理函数的第一件事就是提取请求参数到局部变量中。这里将定义一个工具函数 params.Unpack,使用结构体成员标签直接将参数填充到结构体对应的字段中。由于 URL 的长度有限,因此参数的名称通常比较短,含义也比较模糊。这须要经过成员标签将结构体的字段和参数名称对应上。

在HTTP处理函数中使用

首先,展现这个工具函数的用法。就是假设已经实现了这个 params.Unpack 函数,下面的 search 函数就是一个 HTTP 处理函数,它定义了一个结构体变量 data,data 也定义了成员标签来对应请求参数的名字。Unpack 函数从请求中提取数据来填充这个结构体,这样不只能够更方便的访问,还避免了手动转换类型:

package main

import (
    "fmt"
    "net/http"
)

import "gopl/ch12/params"

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

    // ...其余处理代码...
    fmt.Fprintf(resp, "Search: %+v\n", data)
}

// 这里还缺乏一个 main 函数,最后会补上

工具函数 Unpack 的实现

下面的 Unpack 函数作了三件事情:
1、调用 req.ParseForm() 来解析请求。在这以后,req.Form 就有了全部的请求参数,这个方法对 HTTP 的 GET 和 POST 请求都适用。
2、Unpack 函数构造了一个从每一个有效字段名到对应字段变量的映射。在字段有标签时,有效字段名与实际字段名能够不一样。reflect.Type 的 Field 方法会返回一个 reflect.StructField 类型,这个类型提供了每一个字段的名称、类型以及一个可选的标签。它的 Tag 字段类型为 reflect.StructTag,底层类型为字符串,提供了一个 Get 方法用于解析和提取对于一个特定 key 的子串,好比上面示例中结构体字段后面的 http:"max" 这种形式的字段标签。
3、Unpack 遍历 HTTP 参数中的全部 key\/value 对,而且更新对应的结构体字段。同一个参数能够出现屡次。若是对应的字段是切片,则参数全部的值都会追加到切片里。不然,这个字段会被屡次覆盖,只有最后一次的值才有效。

Unpack 函数的代码以下:

// Unpack 从 HTTP 请求 req 的参数中提取数据填充到 ptr 指向的结构体的各个字段
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }

    // 建立字段映射表,key 为有效名称
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem() // reflect.ValueOf(&x).Elem() 得到任意变量 x 可寻址的值,用于设置值。
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i) // a reflect.StructField,提供了每一个字段的名称、类型以及一个可选的标签
        tag := fieldInfo.Tag           // a reflect.Structtag,底层类型为字符串,提供了一个 Get 方法,下一行就用到了
        name := tag.Get("http")        // Get 方法用于解析和提取对于一个特定 key 的子串
        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 (或者切片字段中的单个元素)。目前,它仅支持字符串、有符号整数和布尔值。要支持其余类型能够再添加:

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
}

执行效果

接着把 search 处理程序添加到一个 Web 服务器中,直接在 search 所在的 main 包的命令源码文件中添加下面的 main 函数:

func main() {
    fmt.Println("http://localhost:8000/search")                                 // Search: {Labels:[] MaxResults:10 Exact:false}
    fmt.Println("http://localhost:8000/search?l=golang&l=gopl")                 // Search: {Labels:[golang gopl] MaxResults:10 Exact:false}
    fmt.Println("http://localhost:8000/search?l=gopl&x=1")                      // Search: {Labels:[gopl] MaxResults:10 Exact:true}
    fmt.Println("http://localhost:8000/search?x=true&max=100&max=200&l=golang") // Search: {Labels:[golang] MaxResults:200 Exact:true}
    fmt.Println("http://localhost:8000/search?q=hello")                         // Search: {Labels:[] MaxResults:10 Exact:false}  # 不存在的参数会忽略
    fmt.Println("http://localhost:8000/search?x=123")                           // x: strconv.ParseBool: parsing "123": invalid syntax  # x 提供的参数解析错误
    fmt.Println("http://localhost:8000/search?max=lots")                        // max: strconv.ParseInt: parsing "lots": invalid syntax  # max 提供的参数解析错误
    http.HandleFunc("/search", search)
    log.Fatal(http.ListenAndServe(":8000", nil))
}

这里提供了几个示例以及输出的结果,直接使用浏览器,输入URL就能返回对应的结果。

显示类型的方法

经过反射的 reflect.Type 来获取一个任意值的类型并枚举它的方法。下面的例子是把类型和方法都打印出来:

package methods

import (
    "fmt"
    "reflect"
    "strings"
)

// Print 输出值 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,表明一个方法值,即一个已经绑定接收者的方法。

下面是两个示例测试,展现以及验证上面的函数:

package methods_test

import (
    "strings"
    "time"

    "gopl/ch12/methods"
)

func ExamplePrintDuration() {
    methods.Print(time.Hour)
    // Output:
    // type time.Duration
    // func (time.Duration) Hours() float64
    // func (time.Duration) Minutes() float64
    // func (time.Duration) Nanoseconds() int64
    // func (time.Duration) Round(time.Duration) time.Duration
    // func (time.Duration) Seconds() float64
    // func (time.Duration) String() string
    // func (time.Duration) Truncate(time.Duration) time.Duration
}

func ExamplePrintReplacer() {
    methods.Print(new(strings.Replacer))
    // Output:
    // type *strings.Replacer
    // func (*strings.Replacer) Replace(string) string
    // func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
}

另外还有一个 reflect.Value.Call 方法,能够调用 Func 类型的 Value,这里没有演示。

注意事项

还有不少反射API,这里的示例展现了反射能作哪些事情。
反射是一个功能和表达能力都很强大的工具,可是要慎用,主要有三个缘由。

代码脆弱

基于反射的代码是很脆弱的。通常编译器在编译时就能报告错误,可是反射错误则要等到执行时才会以崩溃的方式来报告。这多是等待程序运行好久之后才会发生。
好比,尝试读取一个字符串而后填充一个 Int 类型的变量,那么调用 reflect.Value.SetString 就会崩溃。不少使用反射的程序都会有相似的风险。因此对每个 reflect.Value 都须要仔细检查它的类型、是否可寻址、是否可设置。
要回避这种缺陷的最好的办法就是确保反射的使用完整的封装在包里,而且若是可能,在包的 API 中避免使用 reflect.Value,尽可能使用特定的类型来确保输入是合法的值。若是作不到,那就须要在每一个危险的操做前都作额外的动态检查。好比标准库的 fmt.Printf 能够做为一个示例,当遇到操做数类型不合适时,它不会崩溃,而是输出一条描述性的错误消息。这尽管仍然会有 bug,但定位起来就简单多了:

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

反射还下降了自动重构和分析工具的安全性与准确度,由于它们没法检测到类型的信息。

难理解、难维护

类型也算是某种形式的文档,而反射的相关操做则没法作静态类型检查,因此大量使用反射的代码是很难理解的。对应接收 interface{} 或者reflect.Value 的函数,必定要写清楚指望的参数类型和其余限制条件。

运行慢

基于反射的函数会比为特定类型优化的函数慢一到两个数量级。在一个典型的程序中,大部分函数与总体性能无关,因此为了让程序更清晰可使用反射。好比测试就和适合使用反射,由于大部分测试都使用小数据集。但对性能关键路径上的函数,最好避免使用反射。

相关文章
相关标签/搜索