Go语言学习三——对象迷踪

Posted 自由水鸟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言学习三——对象迷踪相关的知识,希望对你有一定的参考价值。

如果你问Java、Go、C、C++、php、Python:你是面向对象语言吗?Java会毫不犹豫的站起来说:没错,作为一门纯粹的面向对象语言,我感到很自豪;C++也不容置疑的点了点头;PHP和Python没有多想,也表达了肯定;血气方刚的Go脸上则洋溢着迷之微笑;老迈的C看了看他们,默不作声。

是否是一门面向对象语言,这个问题对Go来说略显尴尬。一方面,它并没有类似其他面向对象语言中的class关键字用来定义“类”,而且也没有类似于extendsimplement等关键字用来定义类的继承关系,看起来Go并不能算是面向对象的语言;另一方面,通过强大的类型系统,你可以给任意类型(包括基本类型如 int等)添加方法,不同的对象也可以在需要时随时变成孪生兄弟,简而言之,Go可以用它简单而灵活的方式来实现其他面向对象语言所能做到的事,至于是不是一门面向对象语言,你说它是也好,不是也好,不重要。这也是本文标题的含义——对象迷踪,对象在Go中总是不着痕迹,却又随处可见。

本文将讨论经典面向对象语言中的重要特性:对象定义、继承、多态、访问控制等在Go语言中的实现,来对Go中的面向对象编程一探究竟,最后使用Go语言来实现一个简单工厂模式。

类定义

刚才说到,Go其实并没有一个关键字可以用来定义“类”,如果非要说有的话,那就是structstruct的作用和其他语言中的class相差无几,用法上则和C语言中的struct相同。

type People struct {
   Name string
   age int
   Sex byte // 1表示男性,2表示女性
}

func main() {
   peter := &People{"peter", 30, 1}
   lucy := new(People)
   lucy.Name = "lucy"
   lucy.age = 22
   lucy.Sex = 2

   fmt.Println(peter)
   fmt.Println(lucy)
}

上面我们定义了一个People类(如果你不介意这么叫的话),然后用两种方式初始化了两个对象peter和lucy。
在其他语言中,类是关于一个主体的属性和方法的集合,但是在Go的struct中我们只能定义属性,不能定义方法,方法需要额外定义如下:

func (p *People) eat() {
   fmt.Printf("%s eat food\n", p.Name)
}

这个写法初看起来可能有点奇怪,但是一旦我们了解了之后就会觉得很自然,熟悉C++的朋友知道,如果在C++中定义一个成员方法是这样的:

class People {
   private:
       string name;
   int age;
   public:
       void run() {
       cout << name << " eat food" << endl;
       }
};

习惯了这样的写法后,我们可能觉得很正常,但是如果问:run方法中并没有定义name变量,run方法的参数中也没有name变量,为什么run方法竟然可以直接使用name变量呢?是不是有点神奇?

你可能会说,这还不是小菜一碟,因为有this指针啊?run方法其实还可以这样写:

void run() {
   cout << this->name << " eat food" << endl;
}

但是this指针是哪里来的呢?上帝凭空赐予我们的?当然不是,其实是代码中隐藏了run方法的参数this,在这一点上Python的表达更为直接:

class People:
   def __init__(self, name, age, sex):
   self.name = name
   self.age = age
   self.sex = sex

   def eat(self): # 成员方法均包含self参数,同C++和Java中的this
   print "%s eat food" % self.name


peter = People("peter", 30, 1)
peter.eat()

看了这些后,我们可以看出,在Java和C++中,其实代码中隐藏了this指针,这样虽然写起来方便,但是理解起来可能有些困惑,不妨再来看Go中如何给类型添加方法:

func (p *People) eat() {
   fmt.Printf("%s eat food\n", p.Name)
}

方法名前面的(p *People)即声明了该方法属于哪个类型,而且p就是对象指针,名字可以随意,叫this也可以,叫self也可以,看自己心情,在方法中可以使用对象指针获取类型中的成员。一旦这样声明后,eat方法就与类型People产生了联系,成为了类型People的方法。这就是Go实现面向对象的方法,看起来有点简陋,但是却不失灵活和直接。

继承

严格的说,Go是不支持继承的,只能使用组合来获取基类的特性,Go认为这已足够。

依然使用上面People类型的例子,如果我还需要一个Chinese的类型,显然Chinese属于People的一种,也需要吃饭,但是Chinese有自己独特的节日:春节:

type Chinese struct {
   People
}
func (c *Chinese) springFestival() {
   fmt.Printf("%s has spring festival", c.Name)
}

可以看到,Chinese中组合了People类型,随后,在springFestival方法中,就可以直接访问People类型中的属性了,注意到我们并没有给被引用的People命名,属于匿名组合,当然也可以采用命名的方法进行组合,如下:

type Chinese struct {
   p People
}
func (c *Chinese) springFestival() {
   fmt.Printf("%s has spring festival", c.p.Name)
}

多态

Go同样提供了多态的支持,通过提供接口的方式。Go语言中的interface同样是个异类,与其他语言中的interface都不同。

我们先看Java中的接口,通常的用法是先定义一个接口,然后定义一堆实现类来实现该接口,从而形成一个有层级的类型体系,典型的如Java集合中的类。

而Go则反其道而行之,类型与接口之间的关系是松散的,类型可以不依赖接口而单独存在,甚至接口可以在后期需要的时候再定义。

此外还有一个神奇之处是,只要两个接口定义了同样的方法集合,那么他们就是完全相同的接口,即使它们在不同的包下,叫不同的名字。

type IFly interface {
   fly()
}

type IFly2 interface {
   fly()
}

type Bird struct {

}

func (b *Bird) fly() {
   fmt.Println("bird fly")
}

type Plane struct {

}

func (p *Plane) fly() {
   fmt.Println("plane fly")
}

func main() {
   var flyer1 IFly
   var flyer2 IFly2
   flyer1 = new(Bird) // Bird类型赋值给接口IFly
   flyer1.fly()  // flyer1飞行,实际是Bird飞行
   flyer1 = new(Plane)  //  Plane类型赋值给接口IFly
   flyer1.fly()   // flyer1飞行,实际是Plane飞行
   flyer2 = flyer1 // flyer1(此时是Plane实例)可以赋值给IFly2
   flyer2.fly()  // flyer2飞行
}

运行结果如下:

bird fly
plane fly
plane fly

可以看出我们分别将Bird类型的对象和Plane类型的对象赋值给IFly接口,之后调用fly方法时表现出了多态。然后IFly的对象也可以赋值给IFly2,因为IFly和IFly2具有完全相同的方法。

这样有什么好处呢?一是类型和接口之间是松耦合的,可以让我们更专注与业务实现,不必过度关注类型继承关系;二是不用提前设计接口(提前设计接口很多时候并不能预知未来的需求)。

Go语言中的这种灵活的类型设计,个人感觉在类型之间构筑起了一个自由王国:其他语言中的类中充满着等级森严的家族体系,王家的子弟和李家的子弟永远是水火不容,除非有一个更大的家族势力把他们两家组合在一起;而Go语言中大家凭本事吃饭,按技能分工,能写代码的都是一家人,会说相声的彼此认可,你中有我,我中有你,其乐融融……

提到接口,容我再提一下Go语言中的超级接口interface{}

在Go语言中,只要一个类型实现了一个接口中的所有方法,那么这个类型就可以看作接口的实现类,换句话说:任何定义了一个类型中方法集合的子集的接口,都可以看作是这个类型的超类。

举例来说,类型A定义了方法a,b,c,接口I定义了方法a,b,由于{a,b}是{a,b,c}子集,因此类型A就实现了接口I。而interface{}中没有任何方法,相当于是空集,而空集是任何非空集合的子集,因此可以认为任何类型都实现了interface{},基于此,就有了下面这样的用法:

func f(t string) interface{} {
   switch t {
       case "int":
       return 1
       case "string":
       return "1"
       default:
       return nil
   }
}

func main() {
   var t interface{}
   t = f("int")
   fmt.Println(t)
   t = f("string")
   fmt.Println(t)
}

看起来有点像Java中的Object。

访问控制

在添加关键字方面,Go语言从来都是吝啬的,其他面向对象语言很多都有private,public用于控制方法和属性的作用域,而Go中没有。

Go采用了一种很聪明的方式就是通过首字母大小写来控制:首字母大写的方法和属性是public的,在所有包中都可见;首字母小写的方法和属性是包内可见的,其他包无法访问。

例如我们一开始声明的类型People:

type People struct {
   Name string
   age int
   Sex byte // 1表示男性,2表示女性
}

名字和性别是公开的,大家都可以访问,因此首字母是大写的;年龄是个人隐私,除了自己家人知道外,不对其他人暴露。

Go实现简单工厂模式

package pattern

import "fmt"

type CarObj struct {
   brand string
}

type ICar interface {
   Run()
}

type Benz struct {
   CarObj
}

func (b *Benz) Run() {
   fmt.Printf("%s run\n", b.brand)
}

type Bmw struct {
   CarObj
}

func (b *Bmw) Run() {
   fmt.Printf("%s run\n", b.brand)
}

type CarFactory struct {

}

func (factory *CarFactory) CreateCar(t string) ICar {
   switch t {
       case "benz":
       benzCar := new(Benz)
       benzCar.brand = "benz"
       return benzCar
       case "bmw":
       bmwCar := new(Bmw)
       bmwCar.brand = "bmw"
       return bmwCar
       default:
       return nil
   }
}

func main() {
   carFactory := new(pattern.CarFactory)
   benzCar := carFactory.CreateCar("benz")
   benzCar.Run()
   bmw := carFactory.CreateCar("bmw")
   bmw.Run()
}

运行结果如下:

benz run
bmw run

参考资料

  • 《Go语言编程》

  • https://golang.org/doc/faq#Is_Go_an_object-oriented_language


以上是关于Go语言学习三——对象迷踪的主要内容,如果未能解决你的问题,请参考以下文章

你知道的Go切片扩容机制可能是错的

Go 语言 10 岁了!这里有你不知道的 Go 的成长历程

Go语言GC实现原理即源码分析

Go 语言也 10 岁了!这里或许有你不知道的 Go 的成长历程

编程迷踪 : 第一章 - 侦探bash登场

通学go语言系列之面向对象