Go语言与数据库开发:01-11

反射

Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内
在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。反射也可以让
我们将类型本身作为第一类的值类型处理。

Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何用
反射机制的:一个是fmt包提供的字符串格式功能,另一个是类似encoding/json和encoding/xml
提供的针对特定协议的编解码功能。

反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实
现的,但是它们自己的API都没有公开反射相关的接口。

为何需要反射?

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

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

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

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 "???"
}
}

但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的
测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理url.Values等命名的类型
呢?虽然类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配
url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似
url.Values的类型,这会导致对这些库的循环依赖。

没有一种方法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原
因。

reflect.Type和reflect.Value

反射是由 reflect 包提供支持. 它定义了两个重要的类型, Type 和 Value. 一个 Type 表示一个
Go类型. 它是一个接口, 有许多方法来区分类型和检查它们的组件, 例如一个结构体的成员或
一个函数的参数等. 唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5), 同样的实体
标识了动态类型的接口值.
函数 reflect.TypeOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Type:
t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"

其中 TypeOf(3) 调用将值 3 作为 interface{} 类型参数传入.

因为 reflect.TypeOf 返回的是一个动态类型的接口值, 它总是返回具体的类型. 因此, 下面的代
码将打印 "*os.File" 而不是 "io.Writer". 稍后, 我们将看到 reflect.Type 是具有识别接口类型的
表达方式功能的.
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"

要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的. 因为打印动态类型值对于调试和日
志是有帮助的, fmt.Printf 提供了一个简短的 %T 标志参数, 内部使用 reflect.TypeOf 的结果输
出:
fmt.Printf("%Tn", 3) // "int"

reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以持有一个任意类型的值. 函数
reflect.ValueOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Value. 和
reflect.TypeOf 类似, reflect.ValueOf 返回的结果也是对于具体的类型, 但是 reflect.Value 也可
以持有一个接口值.
v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v) // "3"
fmt.Printf("%vn", v) // "3"
fmt.Println(v.String()) // NOTE: ""

和 reflect.Type 类似, reflect.Value 也满足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串,
否则 String 只是返回具体的类型. 相同, 使用 fmt 包的 %v 标志参数, 将使用 reflect.Values 的
结果格式化.
调用 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("%dn", i) // "3"

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

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

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,
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.Interface
return v.Type().String() + " value"
}
}

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,
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.Interface
return v.Type().String() + " value"
}
}
到目前为止, 我们的函数将每个值视作一个不可分割没有内部结构的, 因此它叫 formatAtom.
对于聚合类型(结构体和数组)个接口只是打印类型的值, 对于引用类型(channels, functions,
pointers, slices, 和 maps), 它十六进制打印类型的引用地址. 虽然还不够理想, 但是依然是一个
重大的进步, 并且 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"

Display递归打印
接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函
数,我们只是像构建一个用于调式用的Display函数,给定一个聚合类型x,打印这个值对应的
完整的结构,同时记录每个发现的每个元素的路径。让我们从一个例子开始。
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)

Display函数的输出如下:
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"

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

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包来简化我们的例子实现。

func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalidn", 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 = niln", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = niln", path)
} else {
fmt.Printf("%s.type = %sn", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf("%s = %sn", 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的可以。和往常一
样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添
加“[key]”来表示访问路径。

指针: Elem方法返回指针指向的变量,还是reflect.Value类型。技术指针是nil,这个操作也
是安全的,在这种情况下指针是Invalid无效类型,但是我们可以用IsNil方法来显式地测试一个
空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧
义。
接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来
获取接口对应的动态值,并且打印对应的类型和值。
现在我们的Display函数总算完工了,让我们看看它的表现吧。

type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *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)调用将显示(strangelove电影对应的中文名是《奇爱博
士》):
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 类型:
Display("os.Stderr", os.Stderr)
// 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)) 调用的输出如
下,当然不同环境得到的结果可能有差异:
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"
...

观察下面两个例子的区别:

var i interface{} = 3
Display("i", i)
// Output:
// Display i (int):
// i = 3
Display("&i", &i)
// Output:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3

在第一个例子中,Display函数将调用reflect.ValueOf(i),它返回一个Int类型的值。

reflect.ValueOf总是返回一个值的具体类型,因为它是从一个接口值提取的内容。

在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr
类型。在switch的Ptr分支中,通过调用Elem来返回这个值,返回一个Value来表示i,对应
Interface类型。一个间接获得的Value,就像这一个,可能代表任意类型的值,包括接口类
型。内部的display函数递归调用自身,这次它将打印接口的动态类型和值。
目前的实现,Display如果显示一个带环的数据结构将会陷入死循环,例如首位项链的链表:

// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)

Display会永远不停地进行深度递归打印:
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支持这类带环的数据结构是比较棘手
的,需要增加一个额外的记录访问的路径;代价是昂贵的。

带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例
如,当它遇到一个指针的时候,它只是简单第打印指针的数值。虽然,在打印包含自身的
slice或map时可能遇到困难,但是不保证处理这种是罕见情况却可以避免额外的麻烦。

时间: 2024-10-26 02:44:10

Go语言与数据库开发:01-11的相关文章

Go语言与数据库开发:01-01

一.前言 Google的三位大牛,为了解决在21世纪多核和网络化环境下越来越复杂的编程问题而发明了go语言, 从2007年9月开始设计和实现,于2009年的11月对外正式发布.从版本的发布历史来看,go语言是从 Ken Thompson发明的B语言.Dennis M. Ritchie发明的C语言逐步演化过来的,是C语言家族的成员, 因此很多人将Go语言称为21世纪的C语言. 纵观这几年来的发展趋势,Go语言已经成为云计算.云存储时代最重要的基础编程语言. Go语言有着和C语言类似的语法,但是它不

Go语言与数据库开发:01-09

包和工具 Go语言有超过100个的标准包(译注:可以用 go list std | wc -l 命令查看标准包的具体数 目),标准库为大多数的程序提供了必要的基础构件.在Go的社区,有很多成熟的包被设 计.共享.重用和改进,目前互联网上已经发布了非常多的Go语音开源包,它们可以通过http://godoc.org 检索. Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具. 包简介 任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放 进一个独立的单元以便于

Go语言与数据库开发:01-04

讲完基础数据类型之后,我们接着学习复合数据类型. 复合数据类型,它是以不同的方式组合基本类型可以构造出来的复合数据类型. 几个基本的复合数据类型: 数组,是由同构的元素组成--每个数组元素都是完全相同的类型; 结构体,则是由异构的元素组成的; 数组和结构体都是有固定内存大小的数据结构; slice和map则是动态的数据结构,它们将根据需要动态增长: 数组: 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成. 因为数组的长度是固定的,因此在Go语言中很少直接使用数组.

Go语言与数据库开发:01-02

接下来,开始了解go语言的程序结构,基础要打牢. Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的.变量保存值,简 单的加法和减法运算被组合成较复杂的表达式.基础类型被聚合为数组或结构体等更复杂的 数据结构.然后使用if和for之类的控制语句来组织和控制表达式的执行流程.然后多个语句被 组织到一个个函数中,以便代码的隔离和复用.函数以源文件和包的方式被组织. . 关于命名: 在Go中是区分大小写的:关键字不能用于自定义名字: Go语言的风格是尽量使用短小的名字,对于局部变量尤其

Go语言与数据库开发:01-08

基于共享变量的并发 .竞争条件 在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定. 例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推.在有两个或更多 goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况 下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是 同时发生是没法判断的.当我们能够没有办法自信地确认一个事件是在另一个事件的前面或 者后面发生的

Go语言与数据库开发:01-07

在本节,我们来说一下并发. 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要 Go语言中的并发程序可以用两种手段来实现.尽管Go对并发的支持是众多强力特性之一,但跟踪调试并发程序还是很困难,在线性程序中 形成的直觉往往还会使我们误入歧途. . Goroutines 在Go语言中,每一个并发的执行单元叫作一个goroutine.设想这里的一个程序有两个函数, 一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系.一个线性的程 序会先调用其中的一个函数,然后再调

Go语言与数据库开发:01-03

在本文中,我们将介绍go的基础数据类型. 虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整 数.浮点数.比特数组.内存地址等.进一步将这些数组织在一起,就可表达更多的对象, 例如数据包.像素点.诗歌,甚至其他任何对象.Go语言提供了丰富的数据组织形式,这依 赖于Go语言内置的数据类型.这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构 的便捷性. Go语言将数据类型分为四类:基础类型.复合类型.引用类型和接口类型. 本文将介绍基础类型,包括:数字.字符串和布尔

Go语言与数据库开发:01-06

Go语言包含了对OOP语言的支持,接下来我们来看看Go语言中的方法. 尽管没有被大众所接受的明确的OOP的定义,从我们的理解来讲,一个对象其实也就是一个 简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类 型关联的函数.一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对 象的用户就不需要直接去操作对象,而是借助方法来做这些事情. 在函数声明时,在其名字之前放上一个变量,即是一个方法. 这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了

Go语言与数据库开发:01-10

测试 现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制.其中有两种技术在实践中证明是比较有效的.第一种是代码在被正式部署前需要进行代码评审.第二种则是测试 我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入要验证边界的处理. 软件测试是一个巨大的领域.测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间.和软件测试技术相关