Go 语言提供了一种机制,在编译时不知道类型的状况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操做,这种机制称为反射(reflection)。 json
本篇各章节的主要内容: 数组
关于反射的文章,下面这篇也不错的,条条理比较清晰,能够参考。
Go语言基础之反射:https://www.liwenzhou.com/posts/Go/13_reflect/ 安全
有时候咱们须要编写一个函数,一个有能力统一处理各类值类型的函数。而这些类型可能没法共享同一个接口,也可能布局未知,还有可能这个类型在设计函数的时候还不存在。甚至这个类型会同时存在以上多个或所有的问题。 数据结构
一个熟悉的例子是 fmt.Printf 中的格式化逻辑,它能够输出任意类型的任意值,包括用户自定义的类型。下面尝试写一个与 fmt.Sprint 相似的函数,只接收一个值而后返回字符串,函数名就称为 Sprint。
先用一个类型分支来判断这个参数是否认义了 String 方法,若是有就调用它。而后添加一些 switch 分支来判断参数的动态类型是不是基本类型,再对每种类型采用不一样的格式化操做:ide
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 "???" } }
到此,尚未用到反射。
对于复合数据类型,也能够添加更多的分支。可是好比数组,不用的长度就是不同的类型,因此这样的类型有无限多。另外还有自定义命名的类型。当咱们没法透视一个未知类型的布局时,这段代码就没法继续,如今就须要反射了。 函数
反射功能由 reflect 包提供,它定义了两个重要的类型:工具
reflect.Type 是一个接口,每一个 Type 表示 Go 语言的一个类型。
reflect.TypeOf 函数接受 interface{} 参数,以 reflect.Type 的形式返回动态类型:布局
t := reflect.TypeOf(3) // a reflect.Type fmt.Println(t.String()) // "int" fmt.Println(t) // "int"
由于 reflect.TypeOf 返回一个接口值对应的动态类型,因此它返回的老是具体类型而不是接口类型:post
var w io.Writer = os.Stdout fmt.Println(reflect.TypeOf(w)) // "*os.File"
由于输出一个接口值的动态类型在调试和日志中很经常使用,因此 fmt.Printf 提供了一个简单的方式 %T,内部的实现就是 reflect.TypeOf:ui
fmt.Printf("%T\n", 3) // "int"
reflect.Value 是一个结构体类型,能够包含一个任意类型的值。
reflect.ValueOf 函数接受 interface{} 参数,将接口的动态值以 reflect.Value 的形式返回。与 reflect.TypeOf 相似,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.Value 也知足 fmt.Stringer,但除非 Value 包含的是一个字符串,不然 String 方法的结果仅仅暴露类型。一般,须要 fmt 包的 %v 功能,它会对 reflect.Value 进行特殊处理。
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 有不少方法能够用来分析所包含的值,而不用知道它的类型。
使用反射的技术,第二次尝试写一个通用的格式化函数,此次名称叫: fotmat.Any。
不用类型分支,这里用 reflec.Value 的 Kind 方法来区分不一样的类型。尽管有无限种类型,但类型的分类(kind)只有少数几种:
最后还有一个 Invalid 类型,表示它们尚未任何的值。(reflect.Value 的零值就属于 Invalid 类型。)
package format import ( "reflect" "strconv" ) // Any 把任何值格式化为一个字符串 func Any(value interface{}) string { return formatAtom(reflect.ValueOf(value)) } // formatAtom 格式化一个值,且不分析它的内部结构 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) // ... 浮点数和负数的分支省略了... 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)。对于聚合类型和接口,只输出值的类型。对于引用类型,输出类型和以十六进制表示的引用地址。这个结构仍然不够理想,下一节会继续改进。
由于 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"
接下来改善组合类型的显示。此次再也不实现一个 fmt.Sprint,而是实现一个称为 Display 的调试工具函数,这个函数对给定的一个复杂值x,输出这个复杂值的完整结构,并对找到的每一个元素标上这个元素的路径。
应当尽可能避免在包的 API 里暴露反射的相关内容,以后将定义一个未导出的函数 display 来作真正的递归处理,再暴露 Display,而 Display 则只是一个简单的封装:
func Display(name string, x interface{}) { fmt.Printf("Display %s (%T):\n", name, x) display(name, reflect.ValueOf(x)) }
在 display 中,使用以前定义的 formatAtom 函数来输出基础值,直接就把这个函数搬过来了。使用 reflect. Value 的一些方法来递归展现复杂类型的每一个组成部分。当递归深刻是,path 字符串会增加,表示是如何达到当前值的。
上两节的示例都是在模拟实现 fmt.Sprint,结构都是经过 strconv 包转成字符串而后返回的。这里就直接使用 fmt 包简化了部分逻辑:
package display import ( "fmt" "reflect" "strconv" ) // formatAtom 格式化一个值,且不分析它的内部结构 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) // ... 浮点数和负数的分支省略了... case reflect.Bool: if v.Bool() { return "true" } return "false" 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" } } func Display(name string, x interface{}) { fmt.Printf("Display %s (%T):\n", name, x) display(name, reflect.ValueOf(x)) } 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: // 基本类型、通道、函数 fmt.Printf("%s = %s\n", path, formatAtom(v)) } }
接下来对 display 函数里的类型分支逐一进行分析。
slice与数组
二者的逻辑一致。Len 方法返回元素的个数,Index(i) 会返回第 i 个元素,返回的元素的类型为 reflect.Value(若是i越界会崩溃)。这两个方法与内置的 len(a) 和 a[i] 序列操做类型。在每一个序列上递归调用了 display 函数,只是在路径后追加了 "[i]"。
尽管 reflect.Value 有不少方法,但对于每一个值,只有少许的方法能够安全调用。好比,Index 方法能够在 Slice、Arrar、String 类型的值上安全调用,但对于其余类型则会崩溃。
结构体
NumField 方法能够报告结构中的字段数,Field(i) 会返回第 i 个字段,返回的字段类型为 reflect.Value。字段列表包括了从匿名字段中作了类型提高的字段。 v.Field(i)
是第i个字段的值,v.Type().Field(i)
就是第i个字段的名称,而后再 .name 就是名称的字符串类型。
map
MapKeys 方法返回一个元素类型为 reflect.Value 的 slice,每一个元素都是一个 map 的 key。与日常遍历 map 的结果相似,顺序是不固定的。MapIndex(key) 返回 key 对应的值。这里仍是忽略了一些情形,map 的 key 也多是超出 formatAtom 能处理的合法类型,好比数组、结构体、接口均可以是合法的key。这还须要再修改一点代码,这里就没有作。
指针
Elem 方法返回指针指向的变量,一样也是以 reflect.Value 类型返回。这个方法在指针是 nil 时也能正确处理,但返回的结果属于 Invalid 类型,因此用了 IsNil 来显式检测空指针,方便输出一条合适的消息。为了不歧义,在路径前加了 * 外边再套一层圆括号。
接口
再次使用 IsNil 来判断接口是否为空。而后用 v.Elem() 获取接口的动态值。再打印出对应的类型的值。
如今 Display 已经完成了,立刻就来实际使用一下。使用下面的这样一个复杂的结构体来进行验证:
package main import "gopl/ch12/display" type Movie struct { Title, Subtitle string Year int Color bool Actor map[string]string Oscars []string Sequel *string } func main() { 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.Display("strangelove", strangelove) }
执行后输出以下:
PS G:\Steed\Documents\Go\src\gopl\ch12\desplay_demo> go run main.go Display strangelove (main.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 PS G:\Steed\Documents\Go\src\gopl\ch12\desplay_demo>
调用标准库的内部结构
还可使用 Display 来显示标准库类型的内部结构,好比: *os.File:
display.Display("os.Stderr", os.Stderr)
注意,即便是非导出的字段在反射下也是可见的。
还能够把 Display 做用在 reflect.Value 上,而且观察它如何遍历 *os.File 的类型描述符的内部结构:
display.Display("rV", reflect.ValueOf(os.Stderr))
调用指针
这里注意以下两个例子的差别:
var i interface{} = 3 display.Display("i", i) // 输出: // Display i (int): // i = 3 display.Display("&i", &i) // 输出: // Display &i (*interface {}): // (*&i).type = int // (*&i).value = 3
在第一个例子中,Display 调用 reflect.ValueOf(i),返回值的类型为 int。
在第二个例子中,Display 调用 reflect.ValueOf(&i),返回的类型为 Ptr,而且是一个指向i的指针。在 Display 的 Ptr 分支中,会调用 Elem 方法,返回一个表明变量 i 的 Value,其类型为 Interface。相似这种间接得到的 Value 能够表明任何值,包括这里的接口。这是 display 函数递归调用本身,输出接口的动态类型和动态值。
在当前的这个实现中,Display 在对象图中存在循环引用时不会自行终止。好比出差一个首尾相连的链表:
// 一个指向本身的结构体 type Cycle struct{ Value int; Tail *Cycle } var c Cycle c = Cycle{42, &c} display.Display("c", c)
执行后会输出一个持续增加的展开式:
Display c (main.Cycle): c.Value = 42 (*c.Tail).Value = 42 (*(*c.Tail).Tail).Value = 42 (*(*(*c.Tail).Tail).Tail).Value = 42 (*(*(*(*c.Tail).Tail).Tail).Tail).Value = 42
不少 Go 程序都会包含一些循环引用的数据。让 Display 支持这类成环的数据结构须要些技巧,须要额外记录迄今访问的路径,相应会带来成本。
一个通用的解决方案须要 unsafe 语言特性,在以后的 unsafe 包的示例中,会有对循环引用的处理。
还有一个相对比较容易实现的思路,限制递归的层数。这个不是那么通用,也不是很完美。可是不须要借助 unsafe 就能够实现。
循环引用在 fmt.Sprint 中不构成一个大问题,由于它不多尝试输出整个结构体。好比,当遇到一个指针时,就只简单地输出指针的数字值,这样就不是引用了。但若是遇到一个 slice 或 map 包含自身,它仍是会卡住,只是不值得为了这种罕见的案例而去承担处理循环引用的成本。
Display 如今能够做为一个显示结构化数据的调试工具,只要再稍加修改,就能够用它来对任意 Go 对象进行编码或编排,使之成为适用于进程间通讯的消息。
Go 的标准库已经支持了各类格式,包括:JSON、XML、ASN.1。另外还有一种普遍使用的格式是 Lisp 语言中的 S表达式。与其余格式不一样的是 S表达式还没被 Go 标准库支持,主要是由于它没有一个公认的标准规范。
接下来就要定义一个包用于将任意的 Go 对象编码为 S表达式,它须要支持如下的结构:
42 integer "hello" string (带有Go风格的引号) foo symbol (未用引号括起来的名字) (1 2 3) list (括号包起来的0个或多个元素)
布尔值通常用符号 t 表示真,用空列表 () 或者符号 nil 表示假,但为了简化,这里的实现直接忽略了布尔值。通道和函数也被忽略了,由于它们的状态对于反射来讲是不透明的。这里的实现还忽略了实数、复数和接口。(部分实现能够后续进行添加完善。)
将 Go 语言的类型编码为S表达式的方法以下:
编码用单个递归调用函数 encode 来实现。它的结构上域上一节的 Display 在本质上是一致的:
package sexpr import ( "bytes" "fmt" "reflect" ) 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 把 Go 的值编码为 S 表达式的形式 func Marshal(v interface{}) ([]byte, error) { buf := new(bytes.Buffer) if err := encode(buf, reflect.ValueOf(v)); err != nil { return nil, err } return buf.Bytes(), nil }
Marshal 函数把上面的编码器封装成一个 API,它相似于其余 encoding/... 包里的 API。
继续用上一节验证 Display 的结构体来应用到这里:
package main import ( "fmt" "gopl/ch12/sexpr" "os" ) type Movie struct { Title, Subtitle string Year int // Color bool Actor map[string]string Oscars []string Sequel *string } func main() { 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.)", }, } b, err := sexpr.Marshal(strangelove) if err != nil { fmt.Fprintf(os.Stderr, "sexpr.Marshal err: %v", err) } fmt.Println(string(b)) }
因为如今不支持布尔值,因此会返回错误:
PS H:\Go\src\gopl\ch12\sexpr_demo> go run main.go sexpr.Marshal err: unsupported type: bool[]
去掉结构体和数据中的Color字段后就正常了:
PS H:\Go\src\gopl\ch12\sexpr_demo> go run main.go ((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Love the Bomb") (Year 1964) (Actor (("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 ("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (Nomin.)" "Best Picture (Nomin.)")) (Sequel nil)) PS H:\Go\src\gopl\ch12\sexpr_demo>
输出的内容很是紧凑,不适合阅读,不过做为格式化的编码已经实现了。若是要输出一个带缩进和换行的美化的格式,要从新实现一个 encode 函数。
与 fmt.Print、json.Marshal、Display 这些同样,sexpr.Marshal 在遇到循环引用的数据时也会无限循环。
接下来还能够继续实现一个解碼器。不过在那以前,还要先了解一下如何用反射来更新程序中的变量。都在下一篇里。