写点什么

Go 语言 Interface 漫谈

  • 2013-02-20
  • 本文字数:3970 字

    阅读完需:约 13 分钟

一件作品的诞生,通常是一个设计师独立完成的。因为这样,一件建筑也好,画作或者音乐舞蹈也好,才能真实反映出其个性。而正是这种不同于其他同类的独特一面,正是这种发自创造者的灵光一现、但又不会背离创作目的和原始架构的新颖实用之处,才使得创新尤为难得。

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 语言如何简单地用一个界面的概念实现了面向对象和动态类型编程。因为本文只是篇漫笔,并非面面俱到地全面讲述,希望读者朋友们能对本人的不周甚至荒唐走板之处一笑了之。谢谢。

2013-02-20 02:3916369

评论

发布
暂无评论
发现更多内容

OpenHarmony 3.2 Beta多媒体系列——音视频播放gstreamer

OpenHarmony开发者

OpenHarmony

前后端结合解决Excel海量公式计算的性能问题

葡萄城技术团队

前端 性能 Excel

MatrixOne从入门到实践01——初识MatrixOne

MatrixOrigin

MatrixOrigin MatrixOne

工程团队如何合理地管理数据库访问

Bytebase

DevOps 运维 dba 数据库管理工具 删库保护

《算法》世界一

初学者

算法 网络 11月月更

《算法》世界二

初学者

算法 网络 11月月更

如何用科学的方法“撞大运”? | 学点运气

赵新龙

CTO 创新 与运气竞争

AR手势识别交互,让应用更加“得心应手”

HarmonyOS SDK

HMS Core

深入浅出DDD编程

百度Geek说

架构 后端 领域驱动设计

看完这篇SpringBoot让我在阿里成功涨薪40%,感谢

钟奕礼

Java java程序员 java面试 java编程

创云融达基于 Curve 块存储的智慧税务场景实践

网易数帆

开源 分布式存储 Ceph curve

「风控算法服务平台」高性能在线推理服务设计与实现

京东科技开发者

Python 数据 高性能 风控 风险控制

上海 Meetup | 一键获取 11 大云原生热门开源项目技术分享入场券

阿里巴巴云原生

阿里云 开源 容器 微服务 云原生

react源码分析:深度理解React.Context

flyzz177

React

HarmonyOS 3重磅版本更新,Mate Xs 2等更多设备支持超级中转站!

极客天地

阿里技术风险与效能部负责人张瓅玶:阿里集团深度用云实践

云布道师

云计算

如何给 Fiori Elements 应用添加自定义按钮

汪子熙

前端开发 web开发 Fiori SAP UI5 11月月更

Java对象拷贝原理剖析及最佳实践

京东科技开发者

Java Apache 编程 对象拷贝 srping

MatrixOne从入门到实践02——源码编译

MatrixOrigin

MatrixOrigin MatrixOne

react源码分析:组件的创建和更新

flyzz177

React

使用keytool生成Tomcat证书

源字节1号

算法基础:单链表图解及模板总结

timerring

算法 11月月更 单链表

一个漏测Bug能让你想到多少?

得物技术

测试 测试框架 bug修复 漏洞检测 测试技术

直播预约|Flink + StarRocks 实时数据分析新范式

StarRocks

数据库

MASA Framework 事件总线 - 进程内事件总线

MASA技术团队

Framework MASA Framewrok MASA

SREWorks 数智服务尝鲜,你的数据准备好了吗?

阿里云大数据AI技术

大数据 运维 数据 十一月月更

MatrixOne从入门到实践03——部署MatrixOne

MatrixOrigin

MatrixOrigin MatrixOne

先聊聊「堆栈」,再聊聊「逃逸分析」。Let’s Go!

王中阳Go

Go golang 逃逸分析 内存分配 11月月更

react的useState源码分析

flyzz177

React

Go语言Interface漫谈_Google_樊虹剑_InfoQ精选文章