首页 文章资讯内容详情

Golang OOP、继承、组合、接口

2026-06-01 4 花语

本文内容纲要:

http://www.cnblogs.com/jasonxuli/p/6836399.html

传统OOP概念

OOP(面向对象编程)是对真实世界的一种抽象思维方式,可以在更高的层次上对所涉及到的实体和实体之间的关系进行更好的管理。

流传很广的OOP的三要素是:封装、继承、多态。

对象:可以看做是一些特征的集合,这些特征主要由属性和方法来体现。

封装:划定了对象的边界,也就是定义了对象。

继承:表明了子对象和父对象之间的关系,子对象是对父对象的扩展,实际上,子对象“是”父对象。相当于说“码农是人”。从特征的集合这个意义上说,子对象包含父对象,父对象有的公共特征,子对象全都有。

多态:根据继承的含义,子对象在特性上全包围了父对象,因此,在需要父对象的时候,子对象可以替代父对象。

传统的OOP语言,例如Java,C++,C#,对OOP的实现也各不相同。以Java为例:Java支持extends,也支持interface。这是两种不同的抽象方式。

extends就是继承,AextendsB,表明A是B的一种,是概念上的抽象关系。

ClassHuman{ name:string age:int functioneat(){} functionspeak(){} } ClassManextendsHuman{ functionfish(){} functiondrink(){} }

Golang的OOP

回到Golang。Golang并没有extends,它类似的方式是Embedding。这种方式并不能实现is-a这种定义上的抽象关系,因此Golang并没有传统意义上的多态。

注意下面代码中的绿色粗体注释,把Student当做Human会报错。

packagemain import"fmt" funcmain(){ varhHuman s:=Student{Grade:1,Major:"English",Human:Human{Name:"Jason",Age:12,Being:Being{IsLive:true}}} fmt.Println("student:",s) fmt.Println("student:",s.Name,",isLive:",s.IsLive,",age:",s.Age,",grade:",s.Grade,",major:",s.Major) //h=s//cannotuses(typeStudent)astypeHumaninassignment fmt.Println(h) //Heal(s)//cannotuses(typeStudent)astypeBeinginargumenttoHeal Heal(s.Human.Being)//true s.Drink() s.Eat() } typeCarstruct{ Colorstring SeatCountint } typeBeingstruct{ IsLivebool } typeHumanstruct{ Being Namestring Ageint } func(hHuman)Eat(){ fmt.Println("humaneating...") h.Drink() } func(hHuman)Drink(){ fmt.Println("humandrinking...") } func(hHuman)Move(){ fmt.Println("humanmoving...") } typeStudentstruct{ Human Gradeint Majorstring } func(sStudent)Drink(){ fmt.Println("studentdrinking...") } typeTeacherstruct{ Human Schoolstring Majorstring Gradeint Salaryint } func(sTeacher)Drink(){ fmt.Println("teacherdrinking...") } typeIEatinterface{ Eat() } typeIMoveinterface{ Move() } typeIDrinkinterface{ Drink() } funcHeal(bBeing){ fmt.Println(b.IsLive) }

输出结果:

student:{{{true}Jason12}1English} student:Jason,isLive:true,age:12,grade:1,major:English {{false}0} true studentdrinking... humaneating... humandrinking...

这里有一点需要注意,Student实现了Drink方法,覆盖了Human的Drink,但是没有实现Eat方法。因此,Student在调用Eat方法时,调用的是Human的Eat();而Human的Eat()调用了Human的Drink(),于是我们看到结果中输出的是humandrinking...。这既不同于Java类语言的行为,也不同于prototype链式继承的行为,Golang叫做Embedding,这像是一种寄生关系:Human寄生在Student中,但仍保持一定程度的独立。

Golang的接口

我们从接口产生的原因来考虑。

代码处理的是各种数据。对于强类型语言来说,非常希望一批数据都是单一类型的,这样它们的行为完全一致。但世界是复杂的,很多时候数据可能包含不同的类型,却有一个或多个共同点。这些共同点就是抽象的基础。单一继承关系解决了is-a也就是定义问题,因此可以把子类当做父类来对待。但对于父类不同但又具有某些共同行为的数据,单一继承就不能解决了。单一继承构造的是树状结构,而现实世界中更常见的是网状结构。

于是有了接口。接口是在某一个方面的抽象,也可以看做具有某些相同行为的事物的标签。

但不同于继承,接口是松散的结构,它不和定义绑定。从这一点上来说,DuckType相比传统的extends是更加松耦合的方式,可以同时从多个维度对数据进行抽象,找出它们的共同点,使用同一套逻辑来处理。

Java中的接口方式是先声明后实现的强制模式,比如,你要告诉大家你会英语,并且要会听说读写,你才具有英语这项技能。

interfaceIEnglishSpeaker{ ListenEnglish() ReadEnglish() SpeakEnglish() WriteEnglish() }

Golang不同,你不需要声明你会英语,只要你会听说读写了,你就会英语了。也就是实现决定了概念:如果一个人在学校(有School、Grade、Class这些属性),还会学习(有Study()方法),那么这个人就是个学生。

DuckType更符合人类对现实世界的认知过程:我们总是通过认识不同的个体来进行总结归纳,然后抽象出概念和定义。这基本上就是在软件开发的前期工作,抽象建模。

相比较而言,Java的方式是先定义了关系(接口),然后去实现,这更像是从上帝视角先规划概念产生定义,然后进行造物。

因为interface和object之间的松耦合,Golang有typeassertion这样的方式来判断一个接口是不是某个类型:

value,b:=interface.(Type),value是Type的默认实例;b是bool类型,表明断言是否成立。

//接上面的例子 v1,b:=interface{}(s).(Car) fmt.Println(v1,b) v2,b:=interface{}(s).(Being) fmt.Println(v2,b) v3,b:=interface{}(s).(Human) fmt.Println(v3,b) v4,b:=interface{}(s).(Student) fmt.Println(v4,b) v5,b:=interface{}(s).(IDrink) fmt.Println(v5,b) v6,b:=interface{}(s).(IEat) fmt.Println(v6,b) v7,b:=interface{}(s).(IMove) fmt.Println(v7,b) v8,b:=interface{}(s).(int) fmt.Println(v8,b)

输出结果:

{0}false {false}false {{false}0}false {{{true}Jason12}1English}true {{{true}Jason12}1English}true {{{true}Jason12}1English}true <nil>false 0false

上面的代码中,使用空接口interface{}对s进行了类型转换,因为s是struct,不是interface,而类型断言表达式要求点号左边必须为接口。

常用的方式应该是类似泛型的使用方式:

s1:=Student{Grade:1,Major:"English",Human:Human{Name:"Jason",Age:12,Being:Being{IsLive:true}}} s2:=Student{Grade:1,Major:"English",Human:Human{Name:"Tom",Age:13,Being:Being{IsLive:true}}} s3:=Student{Grade:1,Major:"English",Human:Human{Name:"Mike",Age:14,Being:Being{IsLive:true}}} t1:=Teacher{Grade:1,Major:"English",Salary:2000,Human:Human{Name:"Michael",Age:34,Being:Being{IsLive:true}}} t2:=Teacher{Grade:1,Major:"English",Salary:3000,Human:Human{Name:"Tony",Age:31,Being:Being{IsLive:true}}} t3:=Teacher{Grade:1,Major:"English",Salary:4000,Human:Human{Name:"Ivy",Age:40,Being:Being{IsLive:true}}} drinkers:=[]IDrink{s1,s2,s3,t1,t2,t3} for_,v:=rangedrinkers{ switcht:=v.(type){ caseStudent: fmt.Println(t.Name,"isaStudent,he/sheneedsmorehomework.") caseTeacher: fmt.Println(t.Name,"isaTeacher,he/sheneedsmorejobs.") default: fmt.Println("InvalidHumanbeing:",t) } }

输出结果:

JasonisaStudent,he/sheneedsmorehomework. TomisaStudent,he/sheneedsmorehomework. MikeisaStudent,he/sheneedsmorehomework. MichaelisaTeacher,he/sheneedsmorejobs. TonyisaTeacher,he/sheneedsmorejobs. IvyisaTeacher,he/sheneedsmorejobs.

这段代码中使用了TypeSwitch,这种switch判断的目标是类型。

Golang:接口为重

了解了Golang的OOP相关的基本知识后,难免会有疑问,为什么Golang要用这种“非主流”的方式呢?

Java之父JamesGosling在某次会议上有过这样一次问答:

IonceattendedaJavausergroupmeetingwhereJamesGosling(Java’sinventor)wasthefeaturedspeaker.

DuringthememorableQ&Asession,someoneaskedhim:“IfyoucoulddoJavaoveragain,whatwouldyouchange?”

“I’dleaveoutclasses,”hereplied.Afterthelaughterdieddown,heexplainedthattherealproblemwasn’tclassesperse,butratherimplementationinheritance(theextendsrelationship).Interfaceinheritance(theimplementsrelationship)ispreferable.Youshouldavoidimplementationinheritancewheneverpossible.

大意是:

问:如果你重新做Java,有什么是你想改变的?

答:我会把类(class)丢掉。真正的问题不在于类本身,而在于基于实现的继承(theextendsrelationship)。基于接口的继承(theimplementsrelationship)是更好的选择,你应该在任何可能的时候避免使用实现继承。

我的理解是:实现之间应该少用继承式的强关联,多用接口这种弱关联。接口已经可以在很多方面替代继承的作用,比如多态和泛型。而且接口的关系松散、随意,可以有更高的自由度、更多的抽象角度。

以继承为特点的OOP只是编程世界的一种抽象方式,在Golang的世界里没有继承,只有组合和接口,这看起来更符合Gosling的设想。借用那位老人的话:黑猫白猫,捉住老鼠就是好猫。让我来继续探索吧。

注:刚刚学习Golang不久,后面可能会发现也许某些理解是错误的。随时修正。

本文内容总结:

原文链接:https://www.cnblogs.com/jasonxuli/p/6836399.html