Today's Question:  What does your personal desk look like?        GIVE A SHOUT

Go是一门面向对象的编程语言吗

  sonic0002        2023-05-02 23:41:38       1,009        0    

Golang已经开源了13年,在最近的TIOBE编程语言排名中,于2023年3月再次进入前十名,并比2022年底的排名上升了两个位置。

Go在2022年底提高了2个排名

许多第一次接触Go的开发者来自面向对象的编程语言,比如Java、Ruby等,他们在学习Go后第一个问题通常是:Go是一种面向对象的语言吗?在本文中,我们将探讨这个问题。

追溯

在广为人知的Go编程语言“圣经”《The Go Programming Language》中,有一个Go与其主要祖先编程语言之间的亲缘关系图表。

Go与其主要祖先编程语言的亲缘关系图表

从图表中,我们可以清晰地看到Go语言的“继承谱系”。

  • 从C语言借鉴表达式语法、控制语句、基本数据类型、值参数传递、指针等。
  • 从Oberon-2语言借鉴包、包导入和声明的语法,而Object Oberon则提供了方法声明的语法。
  • CSP-based并发语法则是从Alef语言以及Newsqueak语言借鉴而来。

我们可以看到,从Go的祖先追溯来看,Go并没有完全借鉴像Simula、SmallTalk等纯面向对象语言的思想。

Go诞生于2007年,代码于2009年开源,当时面向对象语言和面向对象编程范式正处于流行之中。然而,Go的设计者们认为,经典的面向对象继承系统对于编程和扩展似乎并没有太多的好处,反而带来更多的限制,因此官方版本并不支持经典的面向对象语法,即基于类和对象实现的封装、继承和多态这三个主要的面向对象特征。

但是这是否意味着Go不是一种面向对象的语言呢?并非如此!具有面向对象机制的Object Oberon也是Go的祖先之一,尽管Object Oberon的面向对象语法与我们今天常见的语法又有所不同。

我也咨询了ChatGPT关于这个问题,并收到了以下回复。

Go是一种支持过程式编程和面向对象编程(OOP)概念的多范式编程语言。然而,Go并不支持像Java或C++等语言中的完整面向对象特性,比如继承和方法重写。相反,Go使用接口和组合来实现类似的功能。因此,一些人会说,Go不是一种严格的面向对象编程语言,而是一种具有一定面向对象能力的过程式语言。

那么,是否有官方的Golang回应这个问题呢?是的,请看以下内容。

官方声音

Go官方在FAQ中对于Go是否是面向对象语言的问题的简要回答。

Go是一种面向对象语言吗?

是和否。虽然Go有类型和方法,并允许面向对象的编程风格,但它没有类型层次结构。在Go中,“接口”的概念提供了一种不同的方法,我们认为这种方法易于使用且在某些方面更为通用。还有一些方法可以将类型嵌入其他类型中,从而提供类似但不完全相同于子类化的功能。此外,Go中的方法比C++或Java中的方法更为通用:它们可以为任何类型的数据定义,甚至是内置类型,例如普通的“未装箱”整数。它们不仅限于结构体(类)。

此外,缺乏类型层次结构使得Go中的“对象”感觉比C++或Java等语言中更加轻量级。

 

“是和否”!我们看到Go官方给出了一个“两全其美”的中庸回答。那么Go社区的看法又是怎样的呢?让我们看看一些代表性的Go社区人士的观点。

社区声音

Jaana Dogan和Steve Francia两位前Go核心团队成员在加入Go团队之前就对于Go是否是面向对象语言有了自己的看法。

Jaana Dogan在《The Go type system for newcomers》一文中的观点是,尽管Go缺乏类型层次结构,但仍被认为是一种面向对象的语言,即“尽管Go缺乏类型层次结构,但仍被认为是一种面向对象的语言”。

稍早一些的是Steve Francia在2014年发表的文章《Is Go an Object Oriented language?》中得出的结论,即没有对象或继承的面向对象编程,也可以称为“没有对象”的面向对象编程模型。

两者表述方式不同但意思相同,即Go支持面向对象编程,但不是通过提供经典的类、对象和类型层次结构来实现。

那么,Go实际上是如何实现面向对象编程支持的呢?我们继续看下去!

Go的“没有对象”的面向对象编程

经典面向对象编程的三大特征是封装、继承和多态,以下我们看看它们在Go中是如何对应的。

1. 封装

封装是将数据和操作它的方法打包成一个抽象数据类型,隐藏实现细节,所有数据只能通过暴露的方法访问和操作。这个抽象数据类型的实例称为对象。Java和C++等经典面向对象语言通过类(class)表达封装概念,通过类的实例映射对象。熟悉Java的人一定记得Thinking in java这本书的第二章的标题:“Everything is an object”。在Java中,所有属性和方法都是在类中定义的。

Go没有类,那么封装的概念是如何发挥作用的呢?当面向对象的初学者进入Go的世界时,他们喜欢“匹配”Go中最接近类的语法元素!于是他们发现了struct类型。

Go中的struct类型提供了抽象出现实中的聚合的能力。struct的定义可以包含一组字段,如果从面向对象的角度来看,也可以认为是属性,我们还可以为struct类型定义方法。在下面的例子中,我们定义了一个名为Point的struct类型,它有一个导出的方法Length。

type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

我们看到,与经典面向对象中定义在类中的方法不同,Go方法的声明不需要放在声明struct类型的花括号中。Length方法和Point类型之间的联系是一个叫做接收者参数的语法元素。

那么,struct是对应于经典面向对象中的类吗?是和否!从数据聚合抽象的角度来看,似乎是这样的,struct类型可以有多个异构类型的字段,表示不同的抽象能力(例如整数类型int可以用来抽象现实世界对象的长度,字符串类型字段可以用来抽象现实世界对象的名称等)。

但从拥有方法的角度来看,不仅struct类型,Go中的所有其他命名类型除了内置类型之外都可以拥有自己的方法,甚至是一个底层类型为int的新类型MyInt。

type MyInt int

func(a MyInt)Add(b int) MyInt {
    return a + MyInt(b)
}

2. 继承

正如之前提到的,Go的设计者在Go语言初期重新评估了对经典面向对象语法概念的支持,最终放弃了对类、对象和类继承层次结构等支持。

在面向对象编程中,提到继承,想到的是子类继承父类的属性和方法,但Go没有像Java中的extends关键字一样的显式继承语法,但Go也以不同的方式提供了对“继承”的支持。这种支持是通过类型嵌入实现的,看一个例子。

type P struct {
    A int
    b string
}

func (P) M1() {
}

func (P) M2() {
}

type Q struct {
    c [5]int
    D float64
}

func (Q) M3() {
}

func (Q) M4() {
}

type T struct {
    P
    Q
    E int
}

func main() {
    var t T
    t.M1()
    t.M2()
    t.M3()
    t.M4()
    println(t.A, t.D, t.E)
}

我们看到,类型T通过嵌入两个类型P和Q,“继承”了P和Q的导出方法(M1到M4)和导出字段(A,D)。

事实上,Go中的这种“继承”机制并不是经典面向对象中的继承,外部类型(T)和嵌入类型(P,Q)之间没有“亲属关系”,P,Q的导出字段和方法只是被提升为T的字段和方法。它本质上是一种组合,在组合中实现了委托模式的实现。T只是一个代理,提供了它可以代理的所有方法,例如示例中的M1~M4方法。当外部世界启动对T的M1方法的调用时,T将调用委托给其内部的P实例来实际执行M1方法。

T和P和Q之间的关系不是is-a,而是has-a,这是经典面向对象理论中的理解方式。

3. 多态性

经典面向对象中的多态性特指运行时多态性,这意味着当调用方法时,根据调用方法的实际对象的类型,调用不同类型的方法实现。

以下是C++中常见多态性的示例。

#include 

class P {
        public:
                virtual void M() = 0;
};

class C1: public P {
        public:
                void M();
};

void C1::M() {
        std::cout << "c1.M()\n";
}

class C2: public P {
        public:
                void M();
};

void C2::M() {
        std::cout << "c2.M()\n";
}

int main() {
        C1 c1;
        C2 c2;
        P *p = &c1;
        p->M(); // c1.M()
        p = &c2;
        p->M(); // c2.M()
}

这段代码相对清晰,有一个父类P和两个子类C1和C2。父类P有一个虚成员函数M,而两个子类C1和C2则分别重写了M成员函数。在main函数中,我们声明了一个指向父类P的指针p,然后将C1和C2的对象实例分别赋值给p,并分别调用了M成员函数。从结果来看,p在运行时实际调用的函数会根据它指向的对象实例的实际类型分别调用C1和C2的M函数。

显然,经典面向对象的多态实现依赖于类型层次结构,那么没有类型层次结构的Go如何实现多态呢?

Go使用接口来实现多态!

相比于经典面向对象语言,Go更加强调行为的聚合和一致性而不是数据。因此,Go基于行为聚合提供了类似鸭子类型的类型适配支持,但相比于像ruby这样的动态语言,Go的静态类型机制还确保了应用鸭子类型时的类型安全性。

Go的接口类型本质上是一组方法(一组行为)。实现了接口类型的所有方法的类型可以作为动态类型分配给接口类型。通过该接口类型的变量调用方法实际上是调用其动态类型的方法实现。看以下例子。

type MyInterface interface {
    M1()
    M2()
    M3()
}

type P struct {
}

func (P) M1() {}
func (P) M2() {}
func (P) M3() {}

type Q int
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}

func main() {
    var p P
    var q Q
    var i MyInterface = p
    i.M1() // P.M1
    i.M2() // P.M2
    i.M3() // P.M3

    i = q
    i.M1() // Q.M1
    i.M2() // Q.M2
    i.M3() // Q.M3
}

使用Go实现多态不需要类型继承层次结构,且耦合度低,更加轻巧易用!

Gopher的“面向对象思维”

到目前为止,来自经典面向对象阵营的人们可能已经发现了在开始使用Go时的“尴尬”原因!这种“尴尬”在于Go支持面向对象的方式与经典面向对象语言的方式不同:经典面向对象思维需要继承层次结构,而Go没有也不需要。

要转变成真正的Gopher面向对象思维并不难,即“优先使用接口,优先使用组合,改变is-a思维习惯为has-a思维习惯”。

总结

现在是时候做出一些结论性的评论了。

  • Go支持面向对象,只不过有不同于经典面向对象的语法和类型系统。
  • 使用Go进行面向对象编程只需要有不同的思维方式。
  • 在使用Go进行面向对象编程时,思考方式是:“优先使用接口,优先使用组合”。

参考

OOP  GOLANG  CHINESE  GO 

Share on Facebook  Share on Twitter  Share on Weibo  Share on Reddit 

  RELATED


  0 COMMENT


No comment for this article.