likes
comments
collection
share

为什么一定要区分Go是不是面向对象的呢?

作者站长头像
站长
· 阅读数 12

本文正在参加「金石计划」

Go面向对象吗?其实不重要!

关于Go,是不是面向对象的语言,网上有过很多分析的文章了,虽说都很对,但大多都在介绍Go语言是如何使用面向对象的三大特性的。而我,觉得官方给出的 Yes and no 的回答,很客观,很灵活。

至于什么是面向对象,我就不多说了,如果一定要说一句的话,我会说:面向对象是一种人类很容易理解的设计思想

经常与之一起出现的,是面向过程,它更符合计算机的思维,也就是顺序思维。

更多关于面向对象的基本理念,我就不多赘述了,下面,先跟我一起来了解一下Go语言的“类”吧。

Go语言的"类"

  • 直接来看两段很简单的代码:
// ---Go 语言的 “类”---
type People struct {
	name string // 成员变量
	age  int
}
// ---Java 的类---
public class People {
    String name; // 成员变量
    int age;
}

在 Go 中,不像其他语言(本文多指Java),通常使用 class 表示为一类数据,它是使用 struct 来表示一类数据,抽象出这一类事物的状态和行为,用结构体来表述。

  • 再来看两段代码
// ---Go 语言的 “成员方法”---
func (p *People) Eat() {
	fmt.Println("叫Ciusyan吃饭了!!")
}
// ---Java 的成员方法---
public class People {
    public void Eat() {
    	System.out.println("Ciusyan 在 Java 中吃饭");
	}
}

如果是强面向对象的语言,比如 Java,所有方法必须定义在类中,即使是静态方法。而 Go 不一样啊,它的所有方法都是单独存在于外界的,但每一个结构体都可以通过一些标识符,明确说明哪些方法属于自己。

  • 最后看两段代码
// --- Go 中的 “构造方法”
// NewDefaultPeople 可以看作是无参构造方法
func NewDefaultPeople() *People {
	return NewPeople("", 0)
}

// NewPeople 可以看作是全参构造函数
func NewPeople(name string, age int) *People  {
	return &People{
		name: name,
		age: age,
	}
}
// --- Java 中的构造方法 ---
public class People {
    // 无参构造函数
    public People() {
        this(null, 0);
    }
	
    // 全参构造函数
    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Go语言,没有构造函数这些概念,但是它也可以写一些函数,当作是构造函数来使用。可以用它们,快速的创建对象。但这里可以说明一下:其实只是我们习惯叫做对象罢了,准确来说,创建的是这个结构体的一个值

但是这里真的想吐槽一下,Go语言没有函数重载,开发的时候,功能一样的方法,还得想一个不一样的名字。真的很不方便。😢

至此,相信你已经了解了Go语言的“类”,那么,它是不是面向对象的语言呢?

Go不是面向对象的

Go是不是面向对象,其实并不重要,但若你一定要我给出一个答案,我会说不是。先别着急反驳我,不妨听听我的观点,观点如下:

  1. Go 没有继承
  2. Go 不能直接多态

Go没有继承

是的,Go 语言没有继承。你可能会说不,下面一段代码,不就是继承吗?

type People struct {
   name string // 看作是成员变量
   age  int
}

func (p *People) Eat() {
	fmt.Println("叫Ciusyan吃饭了!!")
}

// Man 可以看作Man继承了People
type Man struct {
   People
   mustache string
}

func main() {
    m := Man{}
    m.Eat()
    fmt.Printf("The Man name=%s, age=%d, 还有胡子了:%s\n",
            m.name, m.age, m.mustache) 
}

看起来还真的很像勒,Man 没有 age、name 两个属性、Eat()方法,但是它也能直接 Man.name、Man.age ,这不就是继承了People 这个父类的属性和方法了吗?

其实不然,上面看起来很像继承的操作,但仅是一个语法糖,其实上面的代码,在编译的时候,编译器会为我们生成相当于这样的代码:

func main() {
	m := Man{}
	m.People.Eat()
	fmt.Printf("The Man name=%s, age=%d, 还有胡子了:%s\n",
		m.People.name, m.People.age, m.mustache)
}

这样的调用,是不是和调用普通成员没什么两样?都是直接用 "." 来调用的(可直接访问的前提下)

也就是说,上面的写法,和下面这样写,实际上是一样的,只是上面哪里的字段名称是匿名的而已:

// Man 可以看作Man 组合了People
type Man struct {
   p People // 组合 People
   mustache string
}

func main() {
	m := Man{}
	m.p.Eat()
	fmt.Printf("The Man name=%s, age=%d, 还有胡子了:%s\n",
		m.p.name, m.p.age, m.mustache)
}

但这样写,就必须要明确指明,是调用 Man 的 p 属性的 Eat() 方法了。就不能使用上面那样的语法糖了,

好了,综上所述,其实Go语言是通过组合匿名字段的方式达到了类似继承的效果,本质上其实就是一块语法糖。至于Go为什么推崇使用组合,而不使用继承,也是为了更加灵活,删繁就简,便于理解吧!

也不能直接使用多态

而继承本质上是为了使用多态,连继承这么重要的特性都没有,也就不能直接使用多态了。也就不能使用形如下面的定义:

type People struct {}

func (p *People) Eat() {
	fmt.Println("People 吃饭")
}

// Man 可以看作Man继承了People
type Man struct {
   People
}

func main() {
    // 父类的引用或者指针,指向之类对象
    var p People = &Man{} // Error
    p.Eat()
}

这样定义,明显是错误的表述,我们不能直接使用多态。利用多态的思想,衍生出了多少设计模式啊。如果没有多态,很多地方既不符合人类的思维方式(一个类可以有不同的实现),也会少了很多优雅的代码。

综上,既然 Go 的 struct,没有继承,也不能直接使用多态, 我就是可以说,它不是面向对象的语言。但是,这样的回答太牵强了,我自己都不太满意。其实,它是不是面向对象的语言,真的不重要。

Go是面向对象的

我一直在反复强调,Go 语言是不是面向对象的思想,真的不重要,理由如下,希望能给你带来一定的思考:

也能封装

很多人眼中的封装,可能是:

type People struct {
   name string
}

func (p *People) GetName() string  {
   return p.name
}

func (p *People) SetName(name string)  {
   p.name = name
}

func main() {
	p := People{}
	p.SetName("Ciusyan")
	println(p.GetName())
}

上面这样的?调整属性权限,让其不能直接被外界访问,只能通过Geter、和Seter来访问。这确实没错,这的确是封装的一部分内容。但它是次要的部分。封装的主要部分:将具体事务抽象成类的过程。

是的,这个过程才是最重要的,比如你要按照人类的思维来造车,你是如何把一辆车,抽象成一个类(struct)的过程, 才是封装的核心。要造车:

  • 肯定得有底盘吧!
  • 肯定得有发动机吧!
  • 肯定得有车身吧!
type struct Car {
    属性1 底盘
    属性2 发动机
    属性3 车身
}

那这辆车有什么功能呢?

  • 至少能够发动吧!
  • 至少得跑起来吧!
func (c *Car) Start() {
    汽车启动
}

func (c *Car) Run() {
    起床跑起来了!!!
}

上面只是简单的体现了一个抽象的过程,而这个过程,就是在将事物封装映射到代码世界中。而这样的过程,其实也更加符合人类思维。

所以,Go 语言也需要将很多问题抽象成同一类。那么,它是不是面向对象,还重要吗?🤔

它们的方法都会被自己标识

它们的方法都会被自己标识?是什么意思呢?又和面向对象有啥关系呢?

如果一个类所拥有的方法,会被它具体创建的实例给标识。

比如先来看一段面向对象的代码(Java):

// People 类的定义
public class People {
    private String name;
    
    public People(String name) {
        this.name = name;
    }
    
    public void Show() {
        System.out.println("我的名字是:" + this.name);
    }
}

// Main方法中创建两个People对象
public static void main(String[] args) {
    People ciusyan = new People("Ciusyan");
    People cherLin = new People("CherLin");
    ciusyan.Show(); // 我的名字是:Ciusyan
    cherLin.Show(); // 我的名字是:CherLin
}

在这段代码中,我创建了两个People,都调用了它们的Show()方法,正确的展示了它们的名字。

这时候就被编译器惊艳到了啊,Show方法明明是一样的,它怎么那么聪明,知道是谁来调用自己了,他怎么知道是 ciusyan 这个人,调用了自己?把 ciusyan 的名字打印出来了。

其实原理很简单,因为People的Show()方法只有一份,ciusyan这个实例在调用它的时候,会隐式的将自己,作为第一个参数,传入Show()方法中,什么意思呢?

比如下面的伪代码:

// People的Show方法
public void Show(People this) {        
    System.out.println("我的名字是:" + this.name);
}

// Main方法中创建两个People对象
public static void main(String[] args) {
    People ciusyan = new People("Ciusyan");
    People cherLin = new People("CherLin");
    
    // 会隐式的将自己,当作第一个参数,传入Show方法中
    ciusyan.Show(ciusyan); // 我的名字是:Ciusyan
    cherLin.Show(cherLin); // 我的名字是:CherLin
}

看看上面的一段代码,只是我们是显式的传入了Show()方法中,如果是这样,那电脑肯定就知道,是谁这个时候在调用Show()方法了嘛!!

这不就是,类的方法,会被创建的实例给标识吗?说这个有什么用呢?那你再来看看下面Go语言的代码:

type People struct {
	name string
}

func (this *People) Show() {
	println("我的名字是:", this.name)
}

func main() {
	var ciusyan People = People{name: "Ciusyan"}
	var cherLin People = People{name: "CherLin"}
	
	ciusyan.Show() // 我的名字是:Ciusyan
	cherLin.Show() // 我的名字是:CherLin
}

我这里特地将People中Show() 方法 reciver 的名字,用 this 来标识,这不就是和大多数面向对象语言中,this 关键字一样的原理吗?

在Go中,在方法前面,用一个 Reciver,可不仅表示,Show() 这个方法是属于 People 的。还有上面那个例子中一样的作用,用于标识这一次是谁在调用Show()方法。

那么,其他面向对象语言中的this思路,和Go语言基本一模一样,那它是不是面向对象的,还重要吗?🤔

也能实现多态

虽然上面说了不能直接使用多态,但是并没有说不能使用多态啊,在Go 语言中,可以利用接口来实现多态。(当然,Java也可以)

比如下面的代码:

// 人类,接口
type People interface {
    // 工作的行为
	Work()
}

// 男人 “类”
type Man struct {}

// 实现接口的方法
func (m *Man) Work() {
	fmt.Println("Man 正在工作")
}

func main() {
    var p People = &Man{}
    // 现在是男人在工作
    p.Work()
}

我们定义了People 这一 interface,里面有一个Work()方法,那么,是谁实现了这个Work方法,谁拥有这个Work()方法,我们根本不用关心,我们只关心,这个人可以工作(有Work方法)。

比如上面是使用一个男人,Man 来工作的,我们换一个人来继续工作,只需要:

// 女人 “类”
type WoMan struct {}

// 实现接口的方法
func (m *WoMan) Work() {
	fmt.Println("WoMan 正在工作~~~")
}

func main() {
    var p People = &WoMan{}
    // 现在是女人在工作
    p.Work()
}

这里说这个,并不是为了跟你介绍,什么是面向接口,其实,这不就是多态吗?一个 “类”,可以有不同的实现。 不论是 Java、还是 Go,它们的接口interface类型,本质上也是一个类。

那么,既然 Go 也可以使用多态,它还是不是面向对象,还重要吗?🤔

真的不重要!!

综上,我从很多方面,写了一点自己的浅见。只想说,面向对象也只是一种思想,你说Go不是面向对象的语言,你在很多场景下,也得用到相关的思想。所以,它 "Yes and no" 面向对象。希望能够说服你,那么我们一起来总结一下吧~

  • Go 语言中,没有对象(但是开发时通常叫做对象),没有类,也没有继承,也不能直接使用多态。
  • 它通过组合匿名字段的手段,来达到类似继承的效果。
  • 它只能通过接口,来实现的多态,使一个类可以有不同的实现。
  • 将一类事物,抽象成struct的过程,这才是封装的主要部分
  • Go语言 sturct 方法的写法,和其他面向对象语言中 this 的本质很像。

最后,再谈两个问题:

  • 难道Go语言不是面向的语言,你不也要抽象封装吗?
  • 难道Go语言不是面向的语言,你就不使用多态了吗?

以上的两个问题,就算你认为,Go不是面向对象的语言,你在开发的时候,也会用到一些面向对象的思想。Go语言通过一些手段,去掉了面向对象中复杂的部分,但也保留了基本的面向对象特性。所以可以说,它是不是面向对象,真的不重要!!!