golang继承多态使用心得
很多人都说用go代替php或者java的最大短板就是写业务太反人类。经过最近的一些使用,发现确实与常见的java或者c++这种有些区别,在这里说明一下。
go继承多态的现状
go没有类的概念
也没有所谓的继承多态。所以按照常规用法开发相应的业务逻辑,确实不适用。
go只有struct和interface
在go中变量/函数名称大写,表示外部可见,小写就是不可见。可以认为用大小写表示了public和private的概念,没有protect的用法。
非侵入式
只要struct实现了interface所有方法,就自动帮你绑定,认为struct继承了interface,并不需要在struct中明确写出。这种叫做非侵入式继承,各有利弊。
type I1 interface {
Mtest()
}
type S3 struct {
a int
}
// 直接实现接口方法,不用在结构体明确指出
func (s *S3) Mtest() {
fmt.Println(s.a)
}
案例
有一个公共模块A,A中调用函数B,B属于其他模块,根据不同的模块,调用不同模块下实现的B。
C++实现
按照常见的有类概念的函数,实现如下
//.h
class A
{
public:
int b{};
void mfun();
//纯虚函数和虚函数效果一样
virtual void mtest() = 0;
};
class B : public A
{
public:
void mtest() override;
};
//.cpp
void B::mtest()
{
std::cout << b << std::endl;
}
void A::mfun()
{
mtest();
}
//main
void test(A* ma)
{
ma->mfun();
}
int main()
{
B mb;
mb.b = 11;
test(&mb);
return 0;
}
在这里有一个很重要的地方(我们都按照public来设定),B继承了A,那么B中包含了A和B的所有成员函数和变量。实例化B,然后把B传定额一个A(父类)的指针,实际上这个指针指向的还是B的数据,直接调用函数mtest
,仍然会调用到B的实现。这是一个非常好用的地方,增加了开发的便利。
对于公共的内容,写在父类,如果想要重新实现,就写在子类;对于父类的方法,不同子类进行不同实现;由于调用逻辑一致,代码又不用做重复的开发,可以用父类的名称编写一套即可。
比如上面如果有B1 B2等等都继承了A,但是在mtest
做不同实现,void test(A* ma)
方法不用做任何修改。
go实现
interface 参数
//类似于父类S1
type S1 struct {
b int
}
//S1中公共调用的函数
func (s *S1) Mfun() {
s.Mtest()
}
func (s *S1) Mtest() {
fmt.Println("S1")
}
type S2 struct {
S1
}
func (s *S2) Mtest() {
fmt.Println("S2")
}
func test(s *S1) {
s.Mfun()
}
func main() {
ss2 := S2{}
test(&ss2)
}
上面会报错,*S2不能转换为*S1,那么我们用go的interface进行转换,修改为如下
type S1 struct {
b int
}
func (s *S1) Mfun() {
s.Mtest()
}
func (s *S1) Mtest() {
fmt.Println("S1")
}
type S2 struct {
S1
}
func (s *S2) Mtest() {
fmt.Println("S2")
}
func callfun(s interface{}) {
switch s.(type) {
case *S2:
ss := s.(*S2)
ss.Mfun()
}
}
func main() {
ss2 := S2{}
callfun(&ss2)
}
上面不会报错,可以调用,但是打印出来的是S1。因为S2没有实现Mfun
,所以调用的是S1的函数,在S1中调用Mtest
又会默认调用S1的实现。
再做如下修改
func callfun(s interface{}) {
switch s.(type) {
case *S2:
ss := s.(*S2)
ss.Mtest()
}
}
这样是可以了,只是还与S1有关系吗?
interface 接口
先看下面的实现
//类似父类
type Dd struct {
A int
}
//类似父类接口
type F interface {
Fun()
}
//子类实现
type S1 struct {
F
Dd
}
func (s *S1) Fun() {
fmt.Println("111", s.A)
}
//子类实现
type S2 struct {
F
Dd
}
func (s *S2) Fun() {
fmt.Println("222", s.A)
}
//统一调用
func mtest(f1 F) {
f1.Fun()
}
func test() {
s1 := &S1{}
s2 := &S2{}
mtest(s1)
mtest(s2)
}
这样貌似可以实现,但是mtest
不能作为S1/S2父类的一个函数。什么意思呢?就是mtest
只能这样写,不能像C++一样,成为父类的一个函数,然后把子类的实例转为父类传递过去,通过父类的类型调用到子类的实现。
如果把统一调用的函数写到interface F中,那么S1和S2都必须实现这个方法,就相当于相通的代码逻辑实现了两遍。
如果把统一调用的函数写到struct Dd中,那么Dd就必须改为继承interface F,因为Dd中无法调用自己不存在的Fun,如果Dd继承interface F,那么必须实现统一的函数,这样就和上面通过interface 参数
实现一样了,因为通用方法只有父类实现了,那么父类调用的时候会默认调用自己的Fun,就无法满足多态。
把子类作为interface放入到父类中
// 父类接口,用来统一调用子类实现
type F1 interface {
Fun()
}
// 父类,把接口作为一个成员变量,类似于把子类指针作为成员变量
type Dd1 struct {
S F1
}
type S11 struct {
}
type S21 struct {
}
// 子类实现统一接口
func (s *S11) Fun() {
fmt.Println("111")
}
func (s *S21) Fun() {
fmt.Println("222")
}
// 统一调用父类
func mtest1(d1 Dd1) {
d1.S.Fun()
}
func test() {
dd1 := Dd1{}
s1 := &S1{}
dd1.S = s1
mtest1(dd1)
}
这种方法的坏处就是,要创建多次,创建好dd1,还要创建s1进行赋值,如果忘了,就相当于调用了空指针。并且在S11结构体实现的Fun中还不能调用统一的变量。比如都有一个int b,放在哪里都不合适。放在F1中不行,因为接口不允许,放在Dd1中不行,因为S11调用不到(S11没有继承Dd1),放在S11中也不行,那么就不同通用的,每个实现(S21)也要增加这个成员。
总结
go的设定,接口就是接口,结构体就是结构体,A就是A,B就是B,不应出现一个类中函数越来越多,越来越复杂,其他语言通过人为约定控制代码的混乱,go直接从语法自由度上做了限制。
go是为了解决并发、性能和C/C++低级语言的缺陷产生的。这就导致go即灵活,又不灵活。go减少了很多特性,但是平常开发中,难免会遇到各种需求,有时候实现起来反而更麻烦,这就是为什么有人说用go写业务简直是反人类。
go适合做中间件,流媒体、网络数据通信等,逻辑单一,性能要求高。而对于大型复杂的互联网服务端,可能不太合适。