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时可能遇到困难,但是不保证处理这种是罕见情况却可以避免额外的麻烦。

目录
相关文章
|
21天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
34 7
|
21天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
21天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
94 71
|
20天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
101 67
|
1天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
26 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
2天前
|
存储 JSON 测试技术
【HarmonyOS Next开发】云开发-云数据库(二)
实现了云侧和端侧的云数据库创建、更新、修改等操作。这篇文章实现调用云函数对云数据库进行增删改查。
20 9
|
21天前
|
存储 Go
go语言中映射
go语言中映射
33 11
|
22天前
|
Go 索引
go语言使用range关键字
go语言使用range关键字
28 7
|
22天前
|
Go 索引
go语言修改元素
go语言修改元素
28 6
|
13天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数