Golang 面向对象全解
Posted Lejeune
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 面向对象全解相关的知识,希望对你有一定的参考价值。
Golang 面向对象全解
golang的面向对象是不完备的面向对象,大部分人员都是从java转换而来,造成了很多的坑,这一篇文章记录我已知的golang面向对象性质和坑,希望后来者注意。
此文仅是个人在学习过程中摸索的自我总结,无法保证完全理解正确,如有错误,欢迎指正,共同学习!
基础
go中没有class的概念,只有与之类似的struct结构体(很像c语言),还有接口interface
接下来简单对比一下,他与普通面向对象语言的概念上的区别。
类/结构 | 接口 | |
go | struct 结构体 定义阶段只能描述成员变量 后面通过在函数前面增加标识符来完成方法的描述,可读性不如java | interface 接口 只能描述方法接口 |
java | class 类 可以描述成员变量、方法 | interface 接口 可以描述成员与方法接口 |
定义上go的interface只用作一种类型定义,可以认为是方法的集合,我们不需要显式的去声明struct继承了什么接口 然后去实现它,而是先去实现那个方法,所有实现了接口中所描述方法的结构体都认为其实现了这个接口。
这个特性非常有趣,但是貌似也容易出错,无法显式地告诉我们,我们哪些方法还未实现。
而还有一个特殊的接口interface{}被叫做空接口,里面有0个方法,认为所有对象都实现了空接口
在做代码对比之前,我们需要先引入接口类型判断的方法,才能说明问题。
类型判断/转换
-
转换
a:=3 float32(a)
所有类型转换(包括之后的类型转换,和函数参数调用中所声明的类型)严格遵守类型转换后的类型,比如someInterface(Object),这个新对象无法调用原对象当中的首字母大写的成员变量,同时也无法调用没有在someInterface中记录的方法,也就是只能调用someInterface中所记录的方法。
-
枚举判断(在已知类型中判断)
switch interface{}(float32(3)).(type) { case float32: fmt.Prntln("this is float32") } interface{}(3)//所有对象都实现了空接口
注意 ,Object.(type),Object需要是interface{}类型,interface{}指的是空接口,所有对象都实现了这个接口,从而可以使用上面的类型转换使对象变成interface{}类型。这个语法需要与switch配合。
-
类型尝试转换(常用于判断是否实现接口)
语法
newObject,ok:=Object.(someType)
其中Object对象要求是interface{}类型,ok是转换结果,true or false,newObject是转换之后的对象。
golang中的接口和结构体
-
struct结构体
-
结构体定义时只能描述成员变量
-
成员变量以首字母大小写区分公有与私有,首字母大写为公有变量,小写反之。
-
结构体类型声明后,其对象存储的是对象实例
type Person struct { Name string } var p Person=Person{"Jack"}
-
结构体定义完成后,可以通过对正常函数增加前缀的方式完成方法的定义与实现。
//Person类定义同上 //方式一 func (p Person) say(){ fmt.Println(p.Name) } //这样就定义了一个对象方法,其中p是Person对象的实例 // //方式二 func (p *Person) say(){ fmt.Println(p.Name) } //此时p是Person对象的指针,此时p.Name也可以等价为(*p).Name,go自动做了转换,所以两个代码等价 //看起来两者都可以完成类方法的定义与实现,但是在使用上会有不同,将会在坑点里面讲
-
-
Interface 接口
-
接口只能留出方法描述,不做具体实现
-
接口不能描述成员变量
-
Go的面向对象实现
首先面向对象具有三大特性,封装、继承、多态,go均不直接支持这三种特性,但都可以通过别的方式来实现这三种特性。
封装
golang中没有class只有struct结构体,类比c语言。go中成员变量没有权限修饰符,public等,其通过成员变量的变量名开头大小写来控制访问。
type Person struct {
Name string//公有变量
ago int//私有变量
}
继承
go中继承是通过包含来实现的。
包含有两种情况,一种是和java一样,父亲有一个儿子对象,儿子和父亲各自拥有自己的名字,此时的儿子和父亲相当于两个不同的类,只是父亲有一个儿子对象而已。
package main
import "fmt"
type Person struct {
Name string
}
type Say interface {
say()
}
func (p *Person)say() {
fmt.Println("My name is"+p.Name)
}
type Father struct {
son Person
Name string
}
func (f Father)teach() {
fmt.Println("I am "+f.Name+", teaching my son"+f.son.Name)
}
func main() {
p:=Person{"Jack"}
f:=Father{p,"Tom"}
f.teach()
}
上面是java意义下的包含,下面这一种是通过包含实现go的继承,通过传入匿名对象,使得父类的所有成员变量、方法都变为子类的。
package main
import "fmt"
type Person struct {
Name string
}
type Say interface {
say()
}
func (p *Person)say() {
fmt.Println("My name is "+p.Name)
}
type Teacher struct {
Person
}
func (f Teacher)teach() {
fmt.Println("I am a teacher! And My name is "+f.Name)
}
func main() {
t:=Teacher{Person{"Tom"}}
t.say()
t.teach()
}
多态
同一个接口不同实现,这个方法我认为在go中是比较鸡肋的。
package main
import "fmt"
type Person struct {
Name string
}
type Say interface {
say()
}
func (p Person)say() {
fmt.Println("My name is "+p.Name)
}
type Teacher struct {
Name string
}
func (f Teacher)say() {
fmt.Println("I am a teacher! And My name is "+f.Name)
}
func main() {
var t,p Say =Teacher{"Tom"},Person{"Jack"}
t.say()
p.say()
}
坑点
-
接口实现无法被程序员直接知道,只能依赖于ide来告诉程序员这个接口中的方法没有被实现。
-
go在接口类型对象赋值的时候,可以接受其引用或者实例。
package main import ( "fmt" ) type Person struct { Name string } type Say interface { say() } func (p Person)say() { fmt.Println("My name is "+p.Name) } func main(){ //以下两种都可以,但是在大多数情况下,对于接口直接用指针赋值一定不会错 //这两者的不同请看下一个坑点 //var p Say=&Person{"Tom"} var p Say=Person{"Tom"} p.say() }
-
ide判断接口实现与否,严格按照定义的前缀
之前提到函数名前面的类型加不加星号,看起来没有区别。但是当我们将前缀加上星号时,
//上面不变 func (p *Person)say() { fmt.Println("My name is "+p.Name) } func main(){ //这句话报错 //var p Say=Person{"Tom"} //这句话正常 var p Say=&Person{"Tom"}//new(Person)返回的也是指针,也可以使用 p.say() }
这是因为,ide在检测接口是否实现时,严格按照上面的定义类型,现在方法被定义在Person类的指针上,Person实例就无法调用,所以在使用时仅能通过指针调用,加上接口对象能接受指针或实例,所以需要加一个取地址符号。如果上面是没有星号的,说明方法定义在实例上,实例和指针都能对其进行调用。但是不管怎么说,对于接口实例对象加一个取地址符号&,是一定能运行的。建议对于接口类型都直接加上&
-
子类调用父类方法上的异常
package main import ( "fmt" ) type Person struct { Name string } //type Say interface { // say() //} func (p *Person)call() { p.say() } func (p *Person)say() { fmt.Println("My name is "+p.Name) } type Teacher struct { Person } func (t Teacher)say() { fmt.Println("I'm a teacher,"+t.Name) } func main(){ var t Teacher=Teacher{Person{"Tom"}} t.call() }
这段代码去调用了person的say,和java上的结果是不同的,这是反直觉的。
猜测实质是因为go的函数调用严格遵守类型,call函数的形式参数是person 那么他就不知道存在teacher的say函数,所以直接去调用了person的say。
那么如何解决?之前说接口是一类方法的集合,我们调用方法时,先想一想,我们到底需要多少方法,将这个函数剥离出结构体之外 ,单独成为一个函数,形式参数传递所需要的方法组成的接口对象,如下:
package main import ( "fmt" ) type Person struct { Name string } type Say interface { say() } func call(s Say) { s.say() } func (p *Person)say() { fmt.Println("My name is "+p.Name) } type Teacher struct { Person } func (t Teacher)say() { fmt.Println("I'm a teacher,"+t.Name) } func main(){ var t Teacher=Teacher{Person{"Tom"}} call(t) }
但是每一次子类方法重载后都需要写一个外部函数去调用,有时候属实麻烦,我们希望通过继承能够完成像java一样的直接调用重载的接口,可以考虑在实现方法的时候,不去调用他原本所属类的实例,而是通过传入一个方法接口实例,从而达到写一次能继承的效果。
package main import ( "fmt" ) type Person struct { Name string } type Say interface { say() } //注意此处 前缀只用作限制其方法所属,真正实例由方法调用时传入 func (Person)call(s Say) { s.say() } func (p Person)say() { fmt.Println("My name is "+p.Name) } type Teacher struct { Person } func (t Teacher)say() { fmt.Println("I'm a teacher,"+t.Name) } func main(){ var t Teacher=Teacher{Person{"Tom"}} t.call(t) }
不管是哪种方法都对程序员的抽象能力做了更高的要求,程序员需要清晰的知道,方法内部需要调用哪些方法,从而抽象出其接口。如果怕出错,不如在有继承关系的结构体实现时,都通过参数来传递对象。
golang的坑点大多数都在interface上,这是我们学习常规面向对象语言之后所遗留下来的情况,听说go官方并不是很建议用go去面向对象,但是面向对象确实能提高效率。。。我们在go中不能以之前的面向对象思维去看待它。
以上是关于Golang 面向对象全解的主要内容,如果未能解决你的问题,请参考以下文章