一件作品的诞生,通常是一个设计师独立完成的。因为这样,一件建筑也好,画作或者音乐舞蹈也好,才能真实反映出其个性。而正是这种不同于其他同类的独特一面,正是这种发自创造者的灵光一现、但又不会背离创作目的和原始架构的新颖实用之处,才使得创新尤为难得。
Go 语言的诞生,是三个有很强个性的设计师共同完成的。Go 语言的定位,就象三维坐标系中的一个点,在强类型、动态和并发这三个特性维度上,分别代表了 Ken、Robert 和 Rob 三人的创造思维的投影。
当然,这样描述不仅是为了表达 Go 语言有这三个特性,也是为了清晰地说明,这三个特性是正交的,也就是它们是彼此独立的,因此可以同时使用而不会彼此制约。 当然,这样描述只是形象地比喻,并不是说这三个设计师彼此独立不必彼此制约,就可以得到同一个独立完整的 Go 语言架构。恰恰相反,只有三人共同认可的特性,才会出现在 Go 语言规范,才会发布在 Go 语言的实现上。有趣的是,这种三驾马车的设计组合,也是 Lua 语言所采用的。它使 Lua 成功避免了过度设计的陷阱,能够在保持自身苗条的同时也不会洁身自好,而是能不断的自我更新,提高性能。
如果说 Lua 语言的一个特性是其唯一但又灵活高效的 table 复合类型,那 Go 语言的一个特性我认为就是其唯一但又灵活高效的 interface 动态类型。这种类型使 Go 语言在保持强静态类型的安全和高效的同时,也能灵活安全地在不同相容类型之间转换。 在进入正题之前,我们先插播一段轻松的话题。关于 interface 的中文翻译,正统的教育教导我们说是接口。例如,Java 和 C++ 中的对象可以理解为非常自闭的个体或者具有同样遗传基因的同类个体的族谱。此时,接口就能恰如其分地表示:要得到我的遗传基因,必须使用此接口。例如,只有声称和驴马都接口了的那种类,才能自称骡类。接口要在定义类时明确声明。
在 Go 语言里,“接吻需声明(注意口写小了些以便好笑增强记忆)”。所以 Go 的接口和正统的类完全不是一类。为避免误解,也为了和港澳台所代表的国际译法接轨,我倾向于把 interface 翻译为“界面”。当然,这也符合这个英文的词源:inter 是中界 face 是面。
好了。够了。该讲 Go 了。
要理解动态类型,需要从静态开始。Go 和 C 族语言一样,是强静态类型的编译语言。每一个变量必须预先声明其类型,也只有相同类型的变量才能赋值和参与运算。例如:
i := 0 j := 0i
分别声明变量 i 和 j 是整型 int 与复数型 complex128。尽管它们的值都是零,尽管我们确信这两个零可以相加并应该能得到正确的零,Go 的编译器却一定会强烈反对。它认为 i 和 j 不是一类不可以运算。这就是强静态类型编译。它把程序员认为可以做的事情一丝不苟的进行强制类型检查,凡是不符合它的规定的一律不予编译,而是举报错误供作者自我检讨。
如果作者要和编译器讨价还价,就要象律师一样研读 Go 的语言规范,才能明白什么是可以通融的、什么是绝对禁止的。例如,Go 语言规范里规定数值类型之间可以有限度的相互转换,例如,整数和浮点数之间,但不包括到复数类型。如果 j := 0.0 声明 j 是浮点数类型,则 float64(i) + j 就可以在强制把整数型的 i 转换为浮点数类型后,再做相同类型变量之间的加法运算。
学过面向对象编程的读者可能会想:嘿,Go 要是能向 XXX 语言一样支持操作符重载或者继承,就不会再有这种加法运算类型不相容的问题了。
真是没问题了吗?还是说问题被抽象了,遮盖了或者说学者除了要学习不同类型之外还有多学一层不同层次的知识了?是简化了还是更复杂更难琢磨了?相信读者会明辨的。
Go 的面向对象不支持重载也只有有限的继承。目的很明确,Go 是要简化类型系统、尤其是已经被过度复杂化了的面向对象的类的类型系统。
这和界面所代表的动态类型系统有关系吗?或者我们问自己,面向对象复杂的类和类型系统所要解决的问题如何用 Go 语言来表达?静态的类和类型,能动态的 interface 吗?
例如,要实现两个不同类型的形状的面积的加运算,在面向对象的语言里,就需要定义一个基类,让这个鸡肋(谐音)有个方法可以相加,再让每个形状去继承,才可以让编译器知道这些类的形状的类型所继承的那个不是任何具体形状的那类形状声明了没有任何具体操作的取得面积的运算,从而可以通融,从而可以从具体类型自己必须已经重新定义的具体的取得自身面积的方法得到具体的数值,才可以把两个具体而且同类的数值相加从而得到面积之和。
如果学者认为是笔者故意把一个简单的道理说得云山雾罩,那学者同志就真的领会了面向对象的精神。让我们拨云见日吧,看看 Go 的界面是怎样解释这个操作的吧。 “接吻需声明”或者说“界面勿需声明”。例如只要两个形状都有取面积的方法,就可以把它们的面积相加,就这么简单明确,完全不需组织它们到同类的抽象形状,也无法在 Go 里做这种勾当。具体的例子:
package main import "fmt" type square struct{ r int } type circle struct{ r int } func (s square) area() int { return s.r * s.r } func (c circle) area() int { return c.r * 3 } func main() { s := square{1} c := circle{1} fmt.Println(s, c, s.area()+c.area()) }
这里所谓的界面,就是方形 square 和圆形 circle 都有 area()int 这样的方法。 注意,我们要下面要用到界面类型了:
package main import "fmt" type square struct{ r int } type circle struct{ r int } func (s square) area() int { return s.r * s.r } func (c circle) area() int { return c.r * 3 } func main() { s := square{1} c := circle{1} a := [2]interface{}{s, c} fmt.Println(s, c, a) sum := 0 for _, t := range a { switch v := t.(type) { case square: sum += v.area() case circle: sum += v.area() } } fmt.Println(sum) }
变量 a 是 interface{}空界面类型的数组变量,类似 C 语言的 void*,可以把任何类型的值放入其单元。此处我们分别放入单位方形和单位圆形变量 s 和 c 的值。
range 是 Go 的遍历语句,此处的变量 t 被依次赋值为数组 a 的单元值,它们还都是空界面类型,所以我们只需用 switch 测试并转换成具体类型的变量 v,就可以使用这个具体类型所定义的 area 方法,得到相应的面积,并进行求和运算了。
这里提到空界面类型类似 C 语言的 void *空指针类型。实际上,为了能动态地检查类型,就必须让这个指针指向一个结构而不是直接指向对应的具体值。这个结构要同时包括值的类型说明和值本身。例如:
图中的两个实线箭头是从空界面数组类型 a 的两个单元指向它们赋值的两个具体类型的值,分别是 square 类型的变量 s 的值 1,以及 circle 类型的变量 c 的值,刚好也是 1。
由于 s 和 c 赋值给界面类型的变量 a[0] 和 a[1],在内存中,它们不仅仅就只有值。上文说过,界面类型的值实际上是个结构,包括具体值和方法表指针。图中虚线箭头所所示的,就是方法表指针。正是通过这个指针,Go 程序运行时才可以顺藤摸瓜地从一个界面变量得到具体变量的类型和它们实现的方法,从而能够在动态类型检查安全后,才执行对应的方法操作。如果安检不过关,就会 panic,也就是出现运行态异常,就是类似数组越界或者除 0 所产生的那种异常。Go 的程序可以使用 recover 捕捉并处理这些异常,这里就不再详述了。
熟悉面向对象语言内部实现的学者肯定能嗅到虚拟函数表的味道。事实上,正是由于 Go 是强类型的编译语言,这些类型的方法函数或者可以在编译时就静态的确定,从而不需间接调用;或者就是通过界面变量这种编译是静态分配一个间接的带类型的方法指针表,从而在程序运行时再动态的类型检查,然后“多态的”调用方法函数。这里所谓的多态,并不是 Go 语言的概念,但这种面向对象的概念,实际上 Go 语言可以通过界面类型有限地支持的。
在 Go 语言中有一个非常重要的界面类型,也是 Go 语言内置的唯一界面类型,error 类型。而 Go 语言库函数以及使用惯例,是返回这个 error 类型的 nil 值表示没有错误,否则就返回一个具体的值表示特定的错误。例如我们定义一个 Err 类型符合 error 界面,也就是要有一个返回 string 的叫 Error 的方法:
type Err struct {} func (_ *Err) Error() string { return "To err is human" }
当函数报错时,我们就返回这个 Err 类型的值,而没有错误时,就返回 nil。注意 Err 类型的值是 error 界面类型所指向的具体值,而 nil 代表这个 error 不指向任何具体值。所以:
func NoErr(ok bool) error { if !ok { return &Err{} } return nil } func main() { fmt.Println(NoErr(true)) fmt.Println(NoErr(false)) // Output: // <nil> // To err is human } </nil>
但如果我们不小心写了如下的错误例子:
func ToErr(ok bool) error { var e *Err = nil if ok { e = &Err{} } return e }
如果我们错误地返回一个 Err 类型但值为 nil 的具体值,而不是直接返回 nil,就会发现依靠返回的 error 是否是 nil 来判断是否出错不再有效:
func main() { fmt.Println(ToErr(true) == nil) //false fmt.Println(ToErr(false) == nil) //false }
这是因为 nil 也是 Err 类型的有效值,而 Err 类型实现了 error 界面的方法 Error(),所以这个 nil 值也一样会调用 Error() 方法返回“To err is human”这个字符串,而不是 nil。
本文只是提纲挈领地展示了一点点 Go 语言界面类型的特色,并添油加醋了一大堆闲言碎语。相信学者朋友们的智商要比作者敝人的高些,能自己去芜存菁,也能举一反三地明白 Go 语言如何简单地用一个界面的概念实现了面向对象和动态类型编程。因为本文只是篇漫笔,并非面面俱到地全面讲述,希望读者朋友们能对本人的不周甚至荒唐走板之处一笑了之。谢谢。
评论