golang继承多态使用心得[一]
很多人都说用go代替php或者java的最大短板就是写业务太反人类。经过最近的一些使用,发现确实与常见的java或者c++有些区别,在这里说明一下。
go继承多态的现状
go没有类的概念
也没有所谓的继承多态。所以按照常规用法开发相应的业务逻辑,确实不适用。
go只有struct和interface
struct类似于类,interface就是接口。没有c++这种,类里面有纯虚函数、虚函数的概念。
方法不需要写在struct中,定义方法时,指定是哪个struct的,就可以自动绑定。
在go中变量/函数名称大写,表示外部可见,小写就是不可见。可以认为用大小写表示了public和private的概念,没有protect的用法。
非侵入式
只要struct实现了interface所有方法,就自动帮你绑定,认为struct继承了interface,并不需要在struct中明确写出。这种叫做非侵入式继承,各有利弊。
type FI interface {
T()
}
type DS struct {
a int
}
// 直接实现接口方法,不用在结构体明确指出
func (s *DS) T() {
fmt.Println(s.a)
}
案例
有一个公共模块A,A中有一个公共函数T,T中调用函数F,B继承A,并且重新实现了F,但是没有实现T。要求用B调用T的时候,可以调用到B实现的F。
C++实现
按照常见的有类概念的函数,实现如下
//.h
class A
{
public:
int b{};
void T();
//纯虚函数和虚函数效果一样
virtual void F() = 0;
};
class B : public A
{
public:
void F() override;
};
//.cpp
void B::F()
{
std::cout << b << std::endl;
}
void A::T()
{
F();
}
//main
void test(A* ma)
{
ma->T();
}
int main()
{
B mb;
mb.b = 11;
test(&mb);
return 0;
}
我们都按照public来设定,B继承了A,那么B中包含了A和B的所有成员函数和变量。实例化B,然后把B传给一个A(父类)的指针,实际上这个指针指向的还是B的数据,用A这个指针调用函数T
,仍然会调用到B的实现。这是一个非常好用的地方,增加了开发的便利。
把统一调用函数写在父类;在子类对功能函数重新实现;调用逻辑都使用父类指针。
比如上面如果有B1
B2
等等都继承了A
,在F
做不同实现,void test(A* ma)
方法不用做任何修改。
go实现
interface 参数
//类似于父类
type DS struct {
b int
}
//DS中公共调用的函数
func (s *DS) T() {
s.F()
}
func (s *DS) F() {
fmt.Println("DS")
}
type DS1 struct {
DS
}
func (s *DS1) F() {
fmt.Println("DS1")
}
func callfunc(s *DS) {
s.T()
}
func main() {
ds1 := DS1{}
callfunc(&ds1)
}
上面会报错,*DS1不能转换为*DS,那么我们用go的interface进行转换,修改为如下
type DS struct {
b int
}
func (s *DS) T() {
s.F()
}
func (s *DS) F() {
fmt.Println("DS")
}
type DS1 struct {
DS
}
func (s *DS1) F() {
fmt.Println("DS1")
}
func callfun(s interface{}) {
switch s.(type) {
case *DS1:
ss := s.(*DS1)
ss.T()
}
}
func main() {
ds1 := DS1{}
callfun(&ds1)
}
上面不会报错,可以调用,但是打印出来的是DS。因为DS1
没有实现T
,所以调用的是DS
的函数,在DS
中调用F
又会默认调用DS
的实现。
再做如下修改
func callfun(s interface{}) {
switch s.(type) {
case *DS1:
ss := s.(*DS1)
ss.F()
}
}
这样是可以了,只是还与S1有关系吗?
interface 接口
先看下面的实现
//类似父类
type DS struct {
A int
}
//类似父类接口
type FI interface {
F()
}
//子类实现
type DS1 struct {
FI
DS
}
func (s *DS1) F() {
fmt.Println("111", s.A)
}
//子类实现
type DS2 struct {
FI
DS
}
func (s *DS2) F() {
fmt.Println("222", s.A)
}
//统一调用
func T(f1 FI) {
f1.F()
}
func test() {
ds1 := &DS1{}
ds2 := &DS2{}
T(s1)
T(s2)
}
这样貌似可以实现,但是T
不能作为DS1/DS2
父类的一个函数。什么意思呢?就是T
只能这样写,不能像C++一样,成为父类的一个函数,然后把子类的实例转为父类传递过去,通过父类的类型调用到子类的实现。
如果把T
写到interface FI
中,那么DS1
和DS2
都必须实现这个方法,就相当于相通的代码逻辑实现了两遍。
如果把T
写到struct DS
中,那么DS
就必须改为继承interface FI
,不然DS
无法调用F
,如果DS
继承interface FI
,那么必须实现F
,这样就和上面通过interface 参数
实现一样了,因为T
只有DS
实现了,那么DS
调用的时候会默认调用自己的F
,就无法满足多态。
把子类作为interface放入到父类中
// 父类接口,定义重写的函数
type FI interface {
F()
}
// 父类,把接口作为一个成员变量,类似于把子类指针作为成员变量
type DS struct {
S FI
}
//父类实现统一调用函数
func (d *DS) T() {
d.S.F()
}
//子类
type DS1 struct {
}
type DS2 struct {
}
// 子类实现重写方法
func (s *DS1) F() {
fmt.Println("111")
}
func (s *DS2) F() {
fmt.Println("222")
}
func test() {
ds := DS{}
ds1 := &DS1{}
ds.S = ds1
ds.T()
}
这种方法的坏处就是,要创建多次,创建好ds
,还要创建ds1
进行赋值,如果忘了,就相当于调用了空指针。并且在DS1
结构体实现的F
中还不能调用统一的变量。比如都有一个int b
,放在哪里都不合适。放在FI
中不行,因为接口不允许;放在DS
中不行,因为DS1
调用不到(DS1
没有继承DS
);放在DS1
中也不行,因为不同通用,每个实现(DS2
)也要增加这个成员。
总结
go的设定,接口就是接口,结构体就是结构体,A就是A,B就是B,避免一个类中函数越来越多,越来越复杂,其他语言通过人为约定控制代码的混乱,go直接从语法自由度上做了限制。
go是为了解决并发、性能和C/C++低级语言的缺陷产生的,这就导致go即灵活,又不灵活。为了性能,必须要灵活,有指针的设定;为了避免编码错误,又要限制语法自由度,这就是为什么有人说用go写业务简直是反人类。
go适合做中间件,流媒体、网络数据通信等,逻辑单一,性能要求高。而对于大型复杂的互联网服务端,可能不太合适。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏