本篇各章节的主要内容:
- 使用 reflect.Value 来设置值:通过 Elem() 方法获取指针对应的值,然后就可以修改值
- 示例,解码 S 表达式:之前内容的综合运用
- 访问结构体成员标签:像JSON反序列化那样,使用反射获取成员标签,并填充结构体的字段
- 显示类型的方法:通过一个简单的示例,获取任意值的类型,并枚举它的方法,还可以调用这些方法
- 注意事项:慎用反射,原因有三
使用 reflect.Value 来设置值
到目前为止,反射只是用来解析变量值。本节的重点是改变值。
可寻址的值(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() 获取变量需要三步:
- 调用 Addr(),返回一个 Value,其中包含一个指向变量的指针
- 在这个 Value 上调用 interface(),返回一个包含这个指针的 interface{} 值
- 如果知道变量的类型,使用类型断言把空接口转换为一个普通指针
之后,就可以通过这个指针来更新变量了:
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 方法也会使程序崩溃:
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 函数做了三件事情:
一、调用 req.ParseForm() 来解析请求。在这之后,req.Form 就有了所有的请求参数,这个方法对 HTTP 的 GET 和 POST 请求都适用。
二、Unpack 函数构造了一个从每个有效字段名到对应字段变量的映射。在字段有标签时,有效字段名与实际字段名可以不同。reflect.Type 的 Field 方法会返回一个 reflect.StructField 类型,这个类型提供了每个字段的名称、类型以及一个可选的标签。它的 Tag 字段类型为 reflect.StructTag,底层类型为字符串,提供了一个 Get 方法用于解析和提取对于一个特定 key 的子串,比如上面示例中结构体字段后面的 http:"max"
这种形式的字段标签。
三、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 的函数,一定要写清楚期望的参数类型和其他限制条件。
运行慢
基于反射的函数会比为特定类型优化的函数慢一到两个数量级。在一个典型的程序中,大部分函数与整体性能无关,所以为了让程序更清晰可以使用反射。比如测试就和适合使用反射,因为大部分测试都使用小数据集。但对性能关键路径上的函数,最好避免使用反射。