我要偷偷的学Go语言,然后惊呆所有人(第四天)

标题无意冒犯,就是觉得这个广告挺好玩的
上面这张思维导图喜欢就拿走

目录

Go语言中的面向对象

作为一门现代高级编程语言,Go语言当然支持面向对象的编程思想。但是和C++中萌芽初现面向对象以及Java信奉的绝对的面向对象相比,Go语言的面向对象显然是太不合群了。

如果你使用过C++,那么你会知道面向对象的基础是对于(class)的抽象,事实上Java也是这样。如果你对于面向对象是什么都不清楚的话,我觉得你需要看一下下面这篇文章:维基百科-面向对象程序设计

好了,现在开始我认为你了解了基本的面向对象程序设计的知识,那么你会不会也认为Go语言也会有一个名为class的关键字?毕竟C++,Java,Python都是这样的啊。

事实上,Go语言确实没有一个名为class的关键字用来声明一个类,甚至都没有类的概念。Go语言实现面向对象的基石是结构体

接下来我们写一个最简单的面向对象的例子:

package main

import "fmt"

type HumanName struct {
    FirstName   string
    LastName    string
}

type Human struct {
    Name   HumanName
    From   string
}

func (human *Human) Speak() { // 为类型Human声明方法Speak
    fmt.Println("I'm", human.Name.FirstName, human.Name.LastName+", and I came from", human.From)
}

func main() {
    tom := Human{
        Name: HumanName{
            FirstName: "Jupter",
            LastName:  "Tom",
        },
        From: "USA",
    }           // 创建一个Human类型的实例
    tom.Speak() // 调用实例的方法
}

现在你应该恍然大悟了,原来Go语言没有类,只有类型。只不过可以给特定类型绑定上方法。而原来类中的属性就对应复合类型中的字段。只是知道这点就可以让我们进行简单的面向对象编程了。但是这还远远不够,我们还有很多问题没有解释清楚。

8.1 方法

比如绑定在类型上的方法和绑定在类型指针上的方法的区别?Go语言这样一个强类型语言,一定会在这上面有特别的区分吧!

func (human *Human) Speak1() {} // 绑定在类型指针上,为指针方法
func (human Human)  Speak2() {} // 绑定在类型上,为值方法
// 非常奇妙的事情,明明是不同的方法接收者,但是却不能使用同样的方法名

在Go语言官方文档中有关于方法的值接收者和指针接收者的区别

对于没有涉及内存的操作(比如说打印一段话,使用值进行计算),值接收者和指针接收者没有什么区别,毕竟都可以使用.来取得值。

但是如果涉及到了值的改变,也就是对于内存的操作,事情就变得麻烦起来:

package main

import "fmt"

type TestInt int

func (value TestInt) Increase1() {
    value += 1
}

func (pointer *TestInt) Increase2() {
    (*pointer) += 1
}

func main() {
    var number TestInt = 1
    numberPointer := &number
    
    fmt.Println(number) // 1
    number.Increase1()
    fmt.Println(number) // 1
    numberPointer.Increase2()
    fmt.Println(number) // 2
}

很明显,也是我们预料中的结果,因为一个是对于传入的临时值进行了改变,而没有涉及main函数中定义的number变量。而传入指针可以让我们很好的操作内存空间。

我们这个示例中,对于值接收者和指针接收者都有不同的方法。但是对于值/指针调用值方法/指针方法的情况,我们还需要进行探讨。

// 我们只保留值方法
func (value TestInt) Increase() {
    value += 1
}

func main() {
    var number TestInt = 1
    numberPointer := &number

    fmt.Println(number) // 1
    number.Increase()
    fmt.Println(number) // 1
    numberPointer.Increase()
    fmt.Println(number) // 1
}

我们运行这个代码,居然没有任何的错误信息!它照常的运行了。只不过值方法仍然没能改变值的值。使用指针调用值方法的时候,只会传入指针对应的值,而这个转换过程是Go语言内部执行的。实际效果:

(*numberPointer).Increase()

如果我们只保留指针方法呢?结果是显而易见的

func (pointer *TestInt) Increase() {
    (*pointer) += 1
}

func main() {
    var number TestInt = 1
    numberPointer := &number
    
    fmt.Println(number) // 1
    number.Increase()
    fmt.Println(number) // 2
    numberPointer.Increase()
    fmt.Println(number) // 3
}

也就是说,当只有对应的指针方法存在时,会将值的指针传入来调用指针方法,也就是说实际是这样的:

(&number).Increase()

也就是说,Go语言会自动寻找和匹配当前最好的方法匹配策略:

  • 如果同时兼有值方法和指针方法,会分别进行调用
  • 如果只有值方法,会隐式转换指针为值进行调用
  • 如果只有指针方法,会隐式转换值为指针进行调用
8.2 embedding而非extends

说到面向对象的时候,大家就会想到几个词:封装继承多态

不太凑巧,你在Go语言中,可能除了封装都见不到…

传统意义上的继承是指一种is a的关系,比如pig is an animal,猪是一个动物,那么猪就应该继承自动物,动物能做的猪也都能做,而猪又有自己特有的方法和属性。

但是很遗憾,Go语言中并没有继承的概念,有的只是组合。因为面向对象依托于结构体的实现,因此一个新的类型只能通过组合的方式来产生:

type Eyes  string // 眼睛类型
type Mouth string // 嘴类型
type Nose  string // 鼻子类型
type Ears  string // 耳朵类型

func (ears *Ears) Hear(s string) { // 耳朵类型的指针听方法
    fmt.Println(s)
}

type Face struct { // 脸类型,由各个部分拼接而成
    Eyes
    Mouth
    Nose
    Ears
}

func main() {
    face := Face{}
    face.Hear("Golang is the best!") // 获得了耳朵的方法
}

就像上面的例子,Face获得了Ears的方法,和is a的关系相比,更像是一种寄生的关系。

并且,在Go语言中是没有函数重载的。

嵌入和聚合

寄生的关系可以使用两种方式来实现:嵌入和聚合。

type Head struct {
    Organ // 器官
    Face  face
    Hair  hair
}

Organ(器官)类型会有一些方法,比如说被福尔马林浸泡这种,那么我们调用头的浸泡福尔马林方法时,肯定不应该调用head.organ.Formalin(),而是应该调用head.Formalin(),这种将一个类型包含在另一个类型的内部的方式叫做嵌入

相对应的,将一个类型声明为另一个类型的属性的方式叫做聚合,我们调用听的时候,肯定是想要去这样调用:head.ear.Hear(),而不是head.Hear()

在合适的嵌入的时候嵌入,在合适聚合的时候聚合。

8.3 接口

当你看到这个教程的时候,我认为你是一个有一定面向对象编程经验的人,如果你对于接口这个概念不太理解,那么可以先去了解接口的定义和作用。

Go语言的接口类型是对于其他类型行为的抽象和概括,因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象方式我们可以让对象更加灵活和更具有适应性。

如果你对于Python有足够的了解,那么你会知道面向对象思想中的鸭子类型白鹅类型,所谓鸭子类型说的是:

如果一个Obejct走起路来像鸭子,叫起来像鸭子,那么它就可以是一个鸭子。或者只是在我们在意的方面和鸭子相似,我们便可以把它当做鸭子。

比如一个鸭子除羽毛器:它接收一个动物,然后除去它的羽毛。

当然,你可以放一只真的鸭子进去,然后你会得到一个没有羽毛的鸭子。但是你要是放一只山雀应该也没什么问题(大家要保护野生动物),因为除羽毛器不关心动物的品种,只关心有没有羽毛。

而白鹅类型则更像一个DNA检测器:它要求接受和猴子有相同基因片段的生物。那么你放进去一个猴子,它抽了一管血,并且检查出了相同的片段。你把自己放进去大概也不会有什么问题。但是如果你把一个猴子电动玩具放了进去:假设经过人工智能的训练,这个玩具几乎和真正的猴子的行为是一致的,这个DNA检测器可能会坏掉并且把你炸飞。

白鹅类型在乎的不是抽象的行为,而是血统,也就是面向对象中的继承关系。

Go语言实现的是鸭子类型。

接口的声明和定义我们在之前已经讲过了,这里不再赘述,只是分析Go语言中的io.Readerio.Writer来丰富我们对于接口的认识:

io.Reader & io.Writer

首先看看具体的定义:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

啊,这两个接口是多么的简单。事实上这两个是最小粒度的接口,分别实现了读方法和写方法。

如果我们需要一个类型,既可以读,也可以写呢?我们是要声明一个具有读方法和写方法的接口吗?如果Go语言只支持继承的话,可能会演变出两种类型:

    1. 继承自Reader,额外实现了Write()方法的类型
    1. 继承自Writer,额外实现了Read()方法的类型

有点奇怪,定义一个新类型也有点奇怪。

所以我们使用Go语言中嵌入的方法来实现io.ReadWriter接口类型:

type ReadWriter interface {
    Reader
    Writer
}

那么也就是说,我们可以通过嵌入的方式来扩展一个接口。从而可以构建不同粒度的接口。我们可以在我们自己的代码中扩展接口,从而利用许多既有的方法:比如可以给任意一个自定义类型定义好Read()方法,返回一个JSON字符串,从而完成自定义的JSON生成和解析。

接口的显式转换和隐式转换

在Go语言中对于接口类型的互相转换相对于其他类型而言较为宽松。

一般我们在使用除了接口以外的类型时,进行类型转换只能通过强制类型转换来实现,甚至连将一个int类型的值赋值给int64类型都坐不到。而接口和一般类型之间往往需要使用类型断言来实现。

但是接口之间的转换却是十分的方便(相对而言),这里借鉴柴先生书中的一个示例:

var (
    // 隐式转换,*os.File满足io.ReadCloser接口
    a io.ReadCloser = (*os.File)(f)
    // 隐式转换
    b io.Reader     = a
    // 隐式转换
    c io.Closer     = a
    // 显式转化,io.Closer不满足io.Reader接口
    d io.Reader     = c.(io.Reader)
)

接口和接口之间灵活的转换,对于平常被强类型摁在地上摩擦的Go语言程序员而言简直就像是天使一般,但是在幸福的同时往往最可能犯错!接口之间的过于灵活让大家开始担忧无意之间类型适配的问题。

于是大家给特定的接口特有的方法,这样便可以大概率的阻止无意义的适配。

接口是Go语言面向对象编程时的利器,但是使用时也要特别注意!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小夕Coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值