f10@t's blog

闲聊一下Go语言的函数方法和多态机制特点

字数统计: 3.1k阅读时长: 12 min
2021/07/24

想闲聊一下Go语言和Java在函数方法、继承、接口、多态机制等方面的区别,这个两个在面向对象思想方面的设计是有不小区别的,具体在编程上也会有较大的区别。这篇我以Java和Go语言代码作为对比,在几个重要问题上记录下区别,帮助自己适应一下Go语言的特点。

Go语言 -- 对象封装

更加简洁的经典struct

如果我们是在Java里面,我们怎么定义一个对象呢?

1
2
3
4
5
6
public class Person {
protected Integer id = 1;
public String name;
public Integer age;
private String email;
}

上面这段代码我们看Person这个对象有哪些特点呢?

  • 有四个很清晰的属性:id、name、age、email。
  • 四个属性的类型很清晰。
  • 四个属性的关键字protectedpublicprivate很清晰的说明了这四个属性的范围。

要我来说总结的话:清晰明了、非常规矩规范。当然如果是一个Bean的话,那么他就都是private的了,只能通过getter、setter来进行访问。

但是呢?Goer们说了,“Java废话好多啊","代码好长啊”。那同样例子换Go呢?

1
2
3
4
5
6
type Person struct {
id int
Name string
Age int
email string
}

可以看到使用了类似C语言的struct关键字,也是清晰明了,有以下的特点:

  • 使用struct结构体来描述对象

  • 先写变量名再写类型,且无范围修饰符

  • 变量名首字母大小不同

那我说其实看的时候就要细心一点了,Go没有private等这些关键字所以就靠变量名大小写了,比如id属性,它就是个私有属性,你是不能从其他包访问到的。而大写的变量名则可以正常访问。

怎么继承?

我们知道Java是使用extends关键字来实现继承的,我们紧接上面的例子:

1
2
3
public class Superman extends Person {
private String skill;
}

这样Superman这个类就继承了Person类,这代表了:

  • 继承了Person类中的Public属性
  • 继承了Person类中的Public方法
  • 父类是Person

这是一个单一集成的关系,一个类有且只能有一个父类,当然Java有也有抽象类,更重要的是有接口 interface的概念,所以具有良好的多态性,也便于维护。同样的例子我们看看Go语言是怎么继承的:

1
2
3
4
type Superman struct {
Person
skill string
}

好了,这就完了。就直接在struct里写上要继承的类就完事了,emmmm确实很简单,当然看不到的东西我们要讨论一下,写一个类名代表了什么?

  • 父类所有变量都提升为了子类的变量(注意包括私有变量)
  • 父类所有的接口实现也提升为了子类的接口实现

可能你猛的一下就蒙了,这算啥子继承?这不就是复制粘贴么。这也是Go语言多态设计的特点,我最后两章会讨论这个问题。

其实在Go语言可能更喜欢把这个叫嵌入类型(type embedding)而不是继承,有内、外之分。像这个Superman和Person的关系,Person就是Superman的内部类型。其特点如下(《Go in Action》):

通过嵌入类型,与内部类型相关的标识符会提升到外部类型上,这些被提升的标识符就行直接声明在外部类型的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。

如果你是Java来的,我这么说你就更清晰了:Go里对内部类型变量、字段的覆写相当于 -> Java里对父类方法的覆写,叫@Override。所以你感觉上是直接把父类粘贴进去了,实际上还是有点区别的。

怎么定义对象方法?

一个比较大的区别就是定义方法了。Java里面我们有很清晰的Class概念,单个Java对象你什么方法你就都定义在你的Class里,定义好你的方法的范围、参数、返回值即可,好比我们的Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
protected Integer id = 1;
public String name;
public Integer age;
private final String email;

public Person(Integer id, String email) {
this.id = id;
this.email = email;
}

public void say() {
System.out.println("Hello, My name is " + this.name
+ " and My email address is " + this.email);
}
}

你用就完了,当然方法也有静态方法,但是他们都在Person这个Class范围里面,没有多余的标识符,特点:

  • 都在Class范围内
  • 无需是哪个类的方法,因为就定义在Class里就是他的

那我们的Go呢?则刚好反过来了,上面同样的逻辑我们用go来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package pkg1

import "fmt"

type Person struct {
id int
Name string
Age int
email string
}

func (p *Person) say() {
fmt.Printf("Hello! My name is %s and my email address is %s", p.Name, p.email)
}

可以看到,简洁了许多,这里我告诉你say()是Person的一个私有方法,就因为前面有一个接收者。特点:

  • 方法名前有接收者
  • 对象定义和方法定义完全分开
  • 方法依旧使用大小写来控制访问(包外不能访问这个方法)

其作用,类型我们接下来进行讨论。

Go语言 -- 函数定义

接收者?

okk,什么是接收者?即receiver,他代表了这个方法是谁的。其有两种类型:

  • 值类型
  • 指针类型

上面Student的Go例子中我们就使用了指针类型的接收者,如果是值类型则这么写:

1
2
3
func (p Person) say()  {
fmt.Printf("Hello! My name is %s and my email address is %s", p.Name, p.email)
}

看似且运行起来没什么区别,实际上这是完全不同的两个概念。*在Go语言中雨C语言是一样的,是指针的概念,所以接下来我们讨论如何确定接收者什么时候用指针,包括函数参数什么使用用指针。

怎么确定接收者、函数参数用不用指针?

我这里按照《Go In Action》来说说我的看法,实际工程中暂时经验不足,估计另有做法。

《Go In Action》中指出,在确定类型时,应先确定它是原始类型还是非原始类型。这里有一个想法上的小坑,书中也明确的指正了我:

原始类型是指对这些变量进行增加或删除的时候会创建一个新值,你可以理解为原子性的;而非原始类型则在原基础上进行修改的。所以在类型的确定上,我们需要看这个对象它在这方面原始类型还是非原始类型,先说结论:

书中将Go语言类型分为了三大类:

  • 内置类型:数值类型、字符串类型和布尔类型
  • 引用类型:切片、映射、通道、接口
  • 结构类型:结构体

其中:

  • 内置类型和引用类型的方法均为原始类型
  • 结构体类型大部分情况下为非原始类型

内置类型的例子我们看strings包的Trim函数:

image-20210804192353201

可以看到传入的都是参数都是原数据的副本---因为他没使用指针。

引用类型我们看net包的MarshalText函数,其中IP类型定义如下:

image-20210804192736267

image-20210804192710277

可以看到它是一个引用类型,本质是字节切片。而MarshalText方法的接受者就是值类型,这是因为他通过复制(ip.toString()),或者叫新建,来传递了这个变量,只需要这个变量的副本就足够了,不去修改原数据,所以是原始类型,所以不用指针接收者。注意并非是因为不修改所以不用指针接收者,而是因为是原始类型所以不用。你好比net.UnMarshalText方法,他要对一个ip去进行赋值,这并不是复制、新建的操作,而是修改,即非原始类型,这时用指针接收者:

image-20210804193647280

对于结构体,大部分情况下都涉及到修改,所以基本上都是使用指针来充当接收者和返回值,当然,也用值当返回值的,比如书中举了结构体Time对象Now()函数的例子:

image-20210804191607293

可以看到这个函数用来新建一个时间对象,返回的是值类型Time而非指针类型 *Time,原因是什么?是因为时间是不允许被修改的,一个时间节点他创建了是多少就是多少,无需也无法修改,所以是原始类型,故返回值为值类型。

Go语言 -- 非侵入式的接口理念与多态

下面讨论完了面向对象的一部分问题后,我们来看看多态这方面的问题,其中Go语言比较独特的就是这个非侵入式的接口设计。

什么叫非侵入式?

假如说有一个接口,他定义了一个手机应该支持的方法,我们用Java语言来描述:

首先我们去定义这个接口的方法

1
2
3
4
5
6
7
public interface MobileInterface {
void powerOn();

void powerOff();

void display(String text);
}

然后我们现在有华为和小米手机,我们都要实现这个接口标准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Huawei Phone
public class HuaweiPhone implements MobileInterface {
@Override
public void powerOn() {
System.out.println("Huawei Up!");
}

@Override
public void powerOff() {
System.out.println("Huawei Down!");
}

@Override
public void display(String text) {
System.out.println("This is a message");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Xiao Mi Phone
public class XiaoMiPhone implements MobileInterface {
@Override
public void powerOn() {
System.out.println("XiaoMi Up!");
}

@Override
public void powerOff() {
System.out.println("XiaoMi Down!");
}

@Override
public void display(String text) {
System.out.println("This is a message from XiaoMi Screen");
}
}

然后我们在运行一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PhoneDemo {
public static void main(String[] args) {
MobileInterface huawei = new HuaweiPhone();
MobileInterface xiaomi = new XiaoMiPhone();
huawei.powerOn();
huawei.display("Hi");
huawei.powerOff();
System.out.println("######################");
xiaomi.powerOn();
xiaomi.display("Thank You!");
xiaomi.powerOff();
}
}

结果:

image-20210804195155572

可以看到,这就是Java的多态,特点:

  • 自动向上转型、向下转型
  • 同一接口不同实现,都是用implements关键字来声明
  • 便于维护,如要新增方法,则在接口中增加方法、类中新增实现即可
  • 实现一个接口必须实现它的所有方法

另外有一个例子看不出来的:一个类可以实现多个接口、一个接口可以extends多个其他接口、所以接口这方面的能力非常强大。那么go语言相同的逻辑呢?

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
44
45
46
47
48
49
50
51
52
package main

import "fmt"

type MobileInterface interface {
powerOn()
display(text string)
powerOff()
}

type HuaweiPhone struct{}
type XiaoMiPhone struct{}

// Huawei
func (h *HuaweiPhone) powerOn() {
fmt.Println("Huawei Up!")
}

func (h *HuaweiPhone) powerOff() {
fmt.Println("Huawei Down!")
}

func (h *HuaweiPhone) display(text string) {
fmt.Printf("This is a message from Huawei Screen: %s\n", text)
}

// XiaoMi
func (h *XiaoMiPhone) powerOn() {
fmt.Println("Xiaomi Up!")
}

func (h *XiaoMiPhone) powerOff() {
fmt.Println("Xiaomi Down!")
}

func (h *XiaoMiPhone) display(text string) {
fmt.Printf("This is a message from Xiaomi Screen: %s\n", text)
}

func demo(phone MobileInterface) {
phone.powerOn()
phone.display("ohhhhh")
phone.powerOff()
}

func main() {
huawei := &HuaweiPhone{}
xiaomi := &XiaoMiPhone{}
demo(huawei)
fmt.Println("####################")
demo(xiaomi)
}

运行结果:

image-20210804200944270

结果是一样的,观察代码,我们可以看到如下特点:

  • 使用接收者来实现方法即可,并无显式的implements关键字等
  • 也有自动向上向下转型的过程

当然go语言中的接口也支持一个类实现多个接口,一个接口可以内嵌多个其他接口,以此实现多态。

那为什么叫非侵入式?对比Java显式的声明implements我们可以看到,Go语言没有这么做,而是我用哪个方法,我就去实现或者覆写哪个方法,对于我这个对象本身我非常的自由,选择权在我对象的手里而不是你接口或者父类的手里。这样的设计降低了复杂性,我们直接写方法就可以了无需去考虑多余的接口关系,而接口的设计者也无需去考虑谁会实现这个接口。更详细的可以参考go语言接口的优势? - 知乎 (zhihu.com)

CATALOG
  1. 1. Go语言 -- 对象封装
    1. 1.1. 更加简洁的经典struct
    2. 1.2. 怎么继承?
    3. 1.3. 怎么定义对象方法?
  2. 2. Go语言 -- 函数定义
    1. 2.1. 接收者?
    2. 2.2. 怎么确定接收者、函数参数用不用指针?
  3. 3. Go语言 -- 非侵入式的接口理念与多态
    1. 3.1. 什么叫非侵入式?