写点什么

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:3915796

评论

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

3DCAT亮相2024中国国际消费电子博览会,引领AI潮流

3DCAT实时渲染

AIGC解决方案 XR实时云渲染

远程连接mysql报错“Host xxx is not allowed to connect to this MySQL server“解决办法

百度搜索:蓝易云

实时渲染什么意思?实时渲染与一般渲染的区别

3DCAT实时渲染

云渲染 实时渲染 实时云渲染 3D实时渲染

Debian使用systemd自动挂载Samba

百度搜索:蓝易云

Apache Calcite System Catalog 实现探究

端小强

Calcite

AI Market全球首创“反向期权”——引领智能金融新时代,重塑全球交易格局!

科技热闻

大势所趋,数字化转型是企业活下去的必选项

禅道项目管理

团队管理 数字化转型 企业管理 项目管理软件 数字化转型咨询

HarmonyOS NEXT开发实战:实现高效下拉刷新与上拉加载组件(二)刷新核心逻辑与空页面集成

王二蛋和他的张大花

鸿蒙

机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)

汀丶人工智能

人工智能

鸿蒙OS开发秘籍:打造优雅的登录状态管理系统

王二蛋和他的张大花

鸿蒙

物资管理系统(源码+文档+部署+讲解)

深圳亥时科技

HarmonyOS NEXT开发实战:打造高效上拉刷新与下拉加载组件(一)空页面的设计与实现

王二蛋和他的张大花

鸿蒙

苹果电脑可以玩英雄联盟吗?macbook能玩lol?

阿拉灯神丁

游戏 #Mac 苹果电脑 CrossOver Mac下载 CrossOver 24

linux下vsode超级用户运行

百度搜索:蓝易云

C++的异常类型与多级catch匹配

百度搜索:蓝易云

如何让Nginx更安全?

江南一点雨

RTE2024:聚焦Gen AI 时代的 RTE,声网发布 RTE+AI 能力全景图

ToB行业头条

鸿蒙OS开发秘籍:打造优雅的登录状态管理系统

王二蛋和他的张大花

鸿蒙

鸿蒙OS高级技巧:打造个性化动态Swiper效果

王二蛋和他的张大花

鸿蒙

数字身份发展趋势前瞻:零信任

芯盾时代

数字身份 iam 零信任 统一身份管理平台

《使用Gin框架构建分布式应用》阅读笔记:p212-p233

codists

golang gin 编程人 codists

降本60% ,阿里云 EMR StarRocks 全新发布存算分离版本

阿里云大数据AI技术

大数据 Serverless StarRocks 弹性伸缩 EMR

解决:Loading class `com.mysql.jdbc.Driver‘. This is deprecated.

百度搜索:蓝易云

融云IM信息托管服务,用户资料、好友关系、群组信息全覆盖

融云 RongCloud

初学者指南:API 设计的核心步骤与方法

爱吃小舅的鱼

API 设计

如何选择合适的工程项目管理系统?9款详解

爱吃小舅的鱼

工程项目管理系统

taobao.item_get_desc API返回值中的促销信息与活动标签探究

代码忍者

API 接口 pinduoduo API

酒店管理系统(源码+文档+部署+讲解)

深圳亥时科技

CDN节点的作用及加速原理解析

HUODUNYUN

CDN CDN加速 CDN技术 CDN带宽

鸿蒙OS模块化开发实战:独立路由与解耦策略

王二蛋和他的张大花

鸿蒙

LLMs 入门实战系列大全:LLMs应用、领域大模型介绍、大模型常见面经汇总

汀丶人工智能

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