Go语言学习三——对象迷踪
Posted 自由水鸟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言学习三——对象迷踪相关的知识,希望对你有一定的参考价值。
如果你问Java、Go、C、C++、php、Python:你是面向对象语言吗?Java会毫不犹豫的站起来说:没错,作为一门纯粹的面向对象语言,我感到很自豪;C++也不容置疑的点了点头;PHP和Python没有多想,也表达了肯定;血气方刚的Go脸上则洋溢着迷之微笑;老迈的C看了看他们,默不作声。
是否是一门面向对象语言,这个问题对Go来说略显尴尬。一方面,它并没有类似其他面向对象语言中的class
关键字用来定义“类”,而且也没有类似于extends
、implement
等关键字用来定义类的继承关系,看起来Go并不能算是面向对象的语言;另一方面,通过强大的类型系统,你可以给任意类型(包括基本类型如 int等)添加方法,不同的对象也可以在需要时随时变成孪生兄弟,简而言之,Go可以用它简单而灵活的方式来实现其他面向对象语言所能做到的事,至于是不是一门面向对象语言,你说它是也好,不是也好,不重要。这也是本文标题的含义——对象迷踪,对象在Go中总是不着痕迹,却又随处可见。
本文将讨论经典面向对象语言中的重要特性:对象定义、继承、多态、访问控制等在Go语言中的实现,来对Go中的面向对象编程一探究竟,最后使用Go语言来实现一个简单工厂模式。
类定义
刚才说到,Go其实并没有一个关键字可以用来定义“类”,如果非要说有的话,那就是struct
。struct
的作用和其他语言中的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语言学习三——对象迷踪的主要内容,如果未能解决你的问题,请参考以下文章