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適合做中間件,流媒體、網絡數據通信等,邏輯單一,性能要求高。而對於大型複雜的互聯網服務端,可能不太合適。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章