/

Go是面向对象的语言吗?

该篇文章首发于boyn.top,转载请声明

Go语言是面向对象的语言吗?

如果某个开发人员在学习Go之前,对于Java,C#那套面向对象设计方法很熟悉的人员,在学习Go的时候,面对Go中的结构体struct,接口interface等概念,也许会产生疑问:Go语言是一门面向对象的语言吗.这个问题的答案是:Yes and No.

我们来看看官方是怎么说的

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

官网中对于Go是否为一门面向对象的语言这个问题的表述为:

是,也不是.虽然Go语言可以通过定义类型和方法来实现面向对象的设计风格,但是Go是实际上并没有继承这一说法.在Go语言中,interface(接口)这个概念以另外一种角度展现了一种更加易用与通用的设计方法.在Go中,我们可以通过组合,也就是将某个类型放入另外的一个类型中来实现类似继承,让该类型提供有共性但不相同的功能.相比起C++和Java,Go提供了更加通用的定义函数的方法,我们可以指定函数的接受对象(receiver),它可以是任意的类型,包括内建类型,在这里没有任何的限制.

同样的,没有了类型继承,使得Go语言在面向对象编程的方面会显得更加轻量化

对于上面的说法,我们可以知道,Go语言实际上可以作为一种面向对象的语言来使用,但是它并不完全像Java一样是完全的面向对象语言.它缺失了一些面向对象定义中的特性,而又用其他的特性来填补这些缺失.是否更好呢?我个人认为Go语言的设计哲学比起Java来会更加的适合于大型项目的开发,我们往往可以不用在意一些类的继承类图,专注于他们自身的特性,而开发者自身在开发时,也可以通过组合等方式来复用他们的逻辑,这对于双方都是有益的.

但是,我们如果死抱教条不放而死板地使用面向对象特性来开发的话,Go语言难免就会变成一门比较复杂的语言,从而部分地失去了其简洁的特性

怎么用Go完成面向对象特性?

正如我们之前所说,Go语言有其自身的面向对象特性,与C++,Java的面向对象是十分不同的.在这一节中,我们就来看看Go语言是如何使用Go完成面向对象特性的.

首先,我们要了解,对于Go来说,这几个概念对于面向对象是必不可少的:Method,Structure,Interface,Receiver,建议不了解Go基本语法的同学先去看一下Go的基本语法,这篇文章并不是一篇Go语言的入门文章.

我们知道,面向对象的三大特性是封装,继承和多态.接下来,我们就一步步看看怎么用Go来完成这些特性

Go语言绝对不允许循环引入,即 package A import B & package B import A

封装

什么是封装呢?简单来说,我们定义了一个结构体,并定义了它的一些方法后,需要进行一定的抽象与逻辑的抽取,可能会有多个方法会执行一段相同的逻辑,我们就可以把这段逻辑抽出来作为一个函数,但是它并不能直接被外界所感知.在Go语言中,封装是基于包(package)的,而不是基于结构体,在同一个包下,我们可以访问任何定义在这个包中的变量与方法,而在外部的包中,只能访问这个包被导出的结构体与方法.在Go中,导出是一个很简单的事情,我们将type的命名首字母大写,则可以让其导出,而如果将其设为小写,则是不导出.

我们来看看实际的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type User struct {
4Username string
4Password string
4Age int
}

func NewUser(name, password string, age int) *User {
4return &User{
44Username: name,
44Password: password,
44Age: age,
4}
}

func (u *User) SetUsername(username string) {
4u.logForChange("Username", username)
4u.Username = username
}

func (u *User) SetAge(i int) {
4u.logForChange("Age", i)
4u.Age = i
}

func (u *User) SetPassword(password string) {
4u.logForChange("Password", password)
4u.Password = password
}

func (u *User) logForChange(field string, value interface{}) {
4fmt.Printf("%s changed to %v", field, value)
}

在这里,我们定义了一个User结构体与其构造方法,并且设定了若干setter方法,而在每次set值的时候,都会打印一条消息来说明更改的域与内容.在这里,打印日志这个逻辑是可以抽象出来的,但是不能让使用这个包的人直接调用,所以我们可以把他设为不导出,也就是首字母小写

继承

在Go语言中,并没有显式的继承与显式的接口实现(接口实现其实也算是一种继承),Go对于继承,是通过组合来实现的,我们如果在父结构体中引入了匿名的其他结构体,那么它的所有成员变量和方法都可以直接通过父结构体来进行访问,而接口的实现也是隐式的,我们只需要实现某个接口的所有方法,就可以认为是实现了这个接口(如果两个接口都有同样的方法,那么就可以认为是实现了这两个接口,这为多态带来了便利,在下一节中将会详细说明).

在这一小节中,我们同样来看看实际的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type Post struct {
4title string
4content string
4User
}

func (p *Post) details(){
4fmt.Printf("Title: %s\nContent: %s\n User: %s\n",p.title,p.content,p.Username)
}

在这里,我们将上面一小节的User进行重用,他们都在一个包下.

我们看到在Post中,引入了一个匿名成员User,现在,我们可以直接通过Post引用User里面的内容了,如果为了方便辨认,同样可以使用p.User.Username这样的方式引用User的内容

多态

在Go语言中,多态是依靠接口来实现的,我们知道,一个接口是隐式地被实现的,当我们实现了这个接口的所有方法,就认为我们实现了这个接口.

我们需要拓展上面定义的程序,使得文章拥有发送文章的功能.即给文章定义一个post方法,可以通过Poster将文章投递到对应的博客系统中(此处以Hexo和Hugo为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type Poster interface {
4postArticle(p Post) error
}

type HexoPoster struct {
4url string
}

func (h *HexoPoster) postArticle(p Post) error {
4fmt.Println("Posted Article to Hexo on "+h.url)
4return nil
}

type HugoPoster struct {
4url string
}

func (h *HugoPoster) postArticle(p Post) error {
4fmt.Println("Posted Article to Hugo on "+h.url)
4return nil
}

type Post struct {
4title string
4content string
4User
4Poster
}

func (p *Post) post(){
4_ = p.Poster.postArticle(*p)
}

func (p *Post) details(){
4fmt.Printf("Title: %s\nContent: %s\n User: %s\n",p.title,p.content,p.Username)
}

在这个程序中,Post多了一个成员变量Poster,我们在初始化的时候需要定义它,可以是HexoPoster或者HugoPoster.这样我们在调用post()方法的时候,不需要指定特定的博客系统,而是可以动态指定,使得程序的可拓展性更强.

image-20200329142137618

参考文章

  1. https://golang.org/doc/faq#Is_Go_an_object-oriented_language Go语言的官方问答
  2. https://www.ardanlabs.com/blog/2014/05/methods-interfaces-and-embedded-types.html 通过实验来说明方法,接口与embedded-type的关系
  3. https://golangbot.com/learn-golang-series/ 26-28章展示了Go语言的面向对象特性

关于图片和转载

知识共享许可协议
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制,你可以在 技术文章配图指南 一文中找到画图的方法和素材。