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