接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是像构建一个用于调式用的Display函数,给定一个聚合类型x,打印这个值对应的完整的结构,同时记录每个发现的每个元素的路径。让我们从一个例子开始。
Copy e, _ := eval. Parse ( "sqrt(A / pi)" )
Display ( "e" , e)
在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:
Copy 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{}类型的参数:
gopl.io/ch12/display
Copy 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包来简化我们的例子实现。
Copy 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 : // basic types, channels, funcs
fmt. Printf ( " %s = %s \n" , 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]”来表示访问路径。(我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型;数组、结构体和接口都可以作为map的key。针对这种类型,完善key的显示信息是练习12.1的任务。)
指针: Elem方法返回指针指向的变量,还是reflect.Value类型。技术指针是nil,这个操作也是安全的,在这种情况下指针是Invalid无效类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。
接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:
Copy type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map [ string ] string
Oscars [] string
Sequel * string
}
让我们声明一个该类型的变量,然后看看Display函数如何显示它:
Copy 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电影对应的中文名是《奇爱博士》):
Copy 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
类型:
Copy 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))
调用的输出如下,当然不同环境得到的结果可能有差异:
Copy 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"
...
观察下面两个例子的区别:
Copy 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类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个值的具体类型,因为它是从一个接口值提取的内容。
在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,通过调用Elem来返回这个值,返回一个Value来表示i,对应Interface类型。一个间接获得的Value,就像这一个,可能代表任意类型的值,包括接口类型。内部的display函数递归调用自身,这次它将打印接口的动态类型和值。
目前的实现,Display如果显示一个带环的数据结构将会陷入死循环,例如首位项链的链表:
Copy // a struct that points to itself
type Cycle struct { Value int ; Tail * Cycle }
var c Cycle
c = Cycle { 42 , & c}
Display ( "c" , c)
Display会永远不停地进行深度递归打印:
Copy 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支持这类带环的数据结构是比较棘手的,需要增加一个额外的记录访问的路径;代价是昂贵的。一般的解决方案是采用不安全的语言特性,我们将在13.3节看到具体的解决方案。
带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单第打印指针的数值。虽然,在打印包含自身的slice或map时可能遇到困难,但是不保证处理这种是罕见情况却可以避免额外的麻烦。
练习 12.1: 扩展Displayhans,以便它可以显示包含以结构体或数组作为map的key类型的值。
练习 12.2: 增强display函数的稳健性,通过记录边界的步数来确保在超出一定限制前放弃递归。(在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)