您的位置 首页 golang

GoLang:OOP(面向对象)?坑

因为GoLang开发效率匹配Python,而性能可以接近C++,仅仅这两大特点就使得GoLang很快站稳了脚跟,并且使用率和占有率逐步攀升。然而在在实际项目中使用GoLang的时候,还是需要当心!本文就来讲一讲笔者在使用GoLang做面向对象的时候遇到的坑。

本文的代码篇幅会比较多,但是代码 绝!对!不!复!杂!

首先,我们来看一看下面的代码,请问运行的结果是什么呢?

 package main

import "fmt"

type BaseBird struct {
    age int
}
func (this *BaseBird)Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 1
    fmt.Printf("after add: age=%d\n", this.age)
}

type DerivedBird struct {
    BaseBird
}
func (this *DerivedBird) Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 2
    fmt.Printf("after add: age=%d\n", this.age)
}

func main() {
    var b1 BaseBird
    var b2 DerivedBird

    b1 = BaseBird{age: 1}
    b1.Add()

    b2 = DerivedBird{BaseBird{1}}
    b2.Add()
}
  

答案应该比较明显, BaseBird Add() 是每次累加1;而 DerivedBird Add() 则是每次累加2,因此累加完毕的 age 值不相同了:

 before add: age=1
after add: age=2

before add: age=1
after add: age=3
  

趁热打铁,我们继续来看另一组代码:

 package main

import "fmt"

type BaseBird struct {
    age int
}

func (this *BaseBird) Cal()  {
    this.Add()
}
func (this *BaseBird)Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 1
    fmt.Printf("after add: age=%d\n", this.age)
}

type DerivedBird struct {
    BaseBird
}
func (this *DerivedBird) Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 2
    fmt.Printf("after add: age=%d\n", this.age)
}

func main() {
    var b1 BaseBird
    var b2 DerivedBird

    b1 = BaseBird{age: 1}
    b1.Cal()

    b2 = DerivedBird{BaseBird{1}}
    b2.Cal()
}
  

实际运行结果如何呢?

实在按捺不住的,可以自己跑一下代码看看。有兴趣的欢迎继续阅读。


I. OOP:class类 | interface接口

类比C++、JAVA甚至Python,这些高级语言的 OOP(Object Orientation Programming,面向对象编程) 的实现,一个必不可少的前提条件是: Class(类) 和/或者 Interface(接口) 数据结构,然后再来谈 Encapsulation(封装) Inheritance(继承) Polymorphism(多态) 等几个 OOP 重要特性。 封装 是由类内的可见性来保障的,语法关键字有public、protected和private; 继承 ,也就是我们常说的父类和子类,也有教材称为基类(Base Class)和派生类(Derived Class),以C++为例,底层是有重载、重写甚至虚函数等更多语法机制来制定,或者说约束整套继承的规则; 多态 是父类/基类或者说是接口类,用子类/派生类进行实例化后呈现出子类/派生类的行为的特征,以C++为例,底层是通过虚函数的语法机制来做到的。

C++中只有类的概念,语法关键字是 class 或者 struct ,这两者实际作用基本一致:

 /* 
* struct结构,默认为public
*/struct Person {
    // 类成员/类属性
    std::string name;
    int age;
    // 类函数/类方法
    Person(std::string _name, int _age) : name(_name), age(_age) {};
    void Show() { 
        printf("name=%s, age=%d\n", name.c_str(), age); 
    };
};

/* 
* class结构,默认为private
*/class Animal {
    // 类成员/类属性
    std::string name;
    int age;

public:
    // 类函数/类方法
    Animal(std::string _name, int _age) : name(_name), age(_age) {};
    void Show() {
        printf("name=%s, age=%d\n", name.c_str(), age);
    }
};
  

Python其实也只有类的概念,语法关键字是 class

 class Animal:
    def __init__(self, _name, _age):
        self.name = _name
        self.age = _age

    def show(self):
        print("name=%s, age=%d" % (self.name , self.age))  

而JAVA则同时有类和接口的概念,语法关键字分别为 class interface

 /* 
* class 类结构
*/public class Animal {
    String name;
    int age;
    
    public Animal(String _name, int _age) {
        name = _name;
        age = _age;
    }
    public void Show() {
        System.out.println("name=" + name + ", age=" + age); 
    }
}

/* 
* interface 接口结构
*/public interface Animal {
   public void Show();
   public void Eat();
}  

然而,对于GoLang而言,虽然它有语法关键字 struct interface ,前者和C++的 struct 语法不完全相同,仅仅支持在struct的内声明“类成员”而不支持“类方法”;后者的作用倒是和JAVA中的 interface 作用类似。当然,其实GoLang的 struct 是支持“类方法”,只不过用法和C++不同:

 /* 
* struct 结构体结构,可以实现类结构
*/type Animal struct {
    name string
    age int
}
func (this *Animal) Show()  {
    fmt.Printf("name=%s, age=%d\n", this.name, this.age)
}

/* 
* interface 接口结构
*/type Animal interface {
    Show()
    Eat()
}
  

II. GoLang:怎么做OOP?

习惯了C++、JAVA(和Python)的类:用语法关键字 class 修饰类名,在类内定义类成员和类方法;外部可以通过实例化类的对象加”.”(或者”->”)获得它的成员或者方法。

从上面的样例代码来看,GoLang是可以做到 的数据结构的。那么 的3种特性如何来做呢?

封装

GoLang中没有 public protected private 语法关键字,它是通过 大小写字母 来控制可见性的。如果常量、变量、类型、接口、结构、函数等名称是 以大写字母开头 则表示能被其它包访问,其作用相当于 public 以非大写开头 就则不能被其他包访问,其作用相当于 private ,当然,在同一个包内是可以访问的。

继承

GoLang中没有像 “:” implements extends 继承的语法关键字,但是也可以做到类似继承的功能。具体做法如<写在前面>中的代码段做法:“子类”的字段嵌入“父类”即可。大概如同:

 type Base struct {
    // 字段
}

type Derived struct {
    Base, // 直接嵌入即可
}
  

实际上,上述的做法并不是真正的继承,而是 匿名组合 ,因此本质还是 组合 !只不过在调用时,可以直接通过实例化变量访问到”父类“的成员和方法。

多态

GoLang的多态是依靠 interface(接口) 实现的。在GoLang中, interface 其实是一种 duck typing 的类型,被具体的实例类型实例化后可以表现出实例类型的行为特征。以下面的代码为例:

 package main

import "fmt"

type Animal interface {
    Show()
}

type Cat struct {
    name string
    age  int
}
func (this *Cat) Show() {
    fmt.Printf("Cat: name=%s, age=%d\n", this.name, this.age)
}

type Dog struct {
    name string
    age  int
}
func (this *Dog) Show() {
    fmt.Printf("Dog: name=%s, age=%d\n", this.name, this.age)
}

func main() {
    var a1, a2 Animal
    a1 = &Cat{
        name: "kitty",
        age:  2,
    }
    a2 = &Dog{
        name: "sally",
        age:  4,
    }
    a1.Show()
    a2.Show()
}
  

综上,我们可以看到,GoLang设计的理念中就没有怎么考虑 OOP GoLang官方也声称不建议使用继承,鼓励多用组合。 插一句题外话:继承多好啊!


III. GoLang的OOP:坑!

最后,回归<写在前面>的最后一个例子,我们来看最终的运行结果是什么:

 before add: age=1
after add: age=2

before add: age=1
after add: age=2
  

不知道读者是否意外,笔者第一次看到这个结果的时候是震惊的,但是回过头思考了下,其实也很好理解:

在GoLang所谓的“ 继承 ”的做法中,实际上是 匿名组合 。GoLang的组合是静态绑定,或者说GoLang所有的 struct 的方法都是静态绑定。那么在<写在前面>最后一个例子,所谓”父类“ BaseBird 的方法 Cal() 调用的本方法 Add() ,虽然在所谓”子类“ DerivedBird 中重新实现了 Add() ,但是对于”父类“的 Cal() 来说,在编译时期,就已经确定了他访问的是自己的 Add() ,也就是所谓“父类” BaseBird 的。

那么为什么C++中可以做到通过 this 指针访问到子类的方法呢?

虚函数 或者说是 虚函数表 。要知道,即使在C++的继承中,如果被调用的函数没有被 virtual 语法关键字修饰为虚函数的话,最终访问的也还是父类的方法。如下面的例子:

 
class BaseBird {
public:
    int age;
    BaseBird(int _age) : age(_age) {};
    ~BaseBird() = default;
    void Cal() { this->Add(); };
    // virtual void Add() { // 被调用的方法是否为虚函数,结果完全不一样
    void Add() {
        printf("before add, age=%d\n", age);
        age += 1;
        printf("after add, age=%d\n", age);
    };
};
class DerivedBird : public BaseBird {
public:
    DerivedBird(int _age) : BaseBird(_age) {};
    ~DerivedBird() = default;
    void Add() {
        printf("before add, age=%d\n", age);
        age += 2;
        printf("after add, age=%d\n", age);
    };
};

int main()
{
    DerivedBird d(1);
    d.Cal();
    BaseBird b(1);
    b.Cal();
    return 0;
}
  

如果 Add() 被声明为虚函数,那么结果是:

 before add, age=1
after add, age=3

before add, age=1
after add, age=2
  

否则,结果是:

 before add, age=1
after add, age=2

before add, age=1
after add, age=2
  

回归来看GoLang。 如果我们在真实业务场景中,确实存在需要这种设计 – 公共逻辑中有一部分需要执行到不同具体类的逻辑 – 怎么办? 插一句题外话,笔者在网上看到有说法是“假如真的存在这种场景,说明逻辑拆分不对,是伪需求”,目前笔者是完全不认同的!

办法就是用 interface !然后问题又来了: interface 只是一堆方法的集合啊,没有具体逻辑?

以<写在前面>最后的例子为例,可以这么做:

 package main

import "fmt"

type Bird interface {
    Add()
}
func Cal(bird Bird)  {
    bird.Add()
}

type BaseBird struct {
    age int
}
func (this *BaseBird)Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 1
    fmt.Printf("after add: age=%d\n", this.age)
}

type DerivedBird struct {
    BaseBird
}
func (this *DerivedBird) Add()  {
    fmt.Printf("before add: age=%d\n", this.age)
    this.age = this.age + 2
    fmt.Printf("after add: age=%d\n", this.age)
}

func main() {
    var b1, b2 Bird
    b1 = &BaseBird{age:1}
    b2 = &DerivedBird{BaseBird{age:1}}
    Cal(b1)
    Cal(b2)
}
  

运行得到的结果:

 before add: age=1
after add: age=2

before add: age=1
after add: age=3
  

总结:使用GoLang做OOP,需要完全抛弃C++和JAVA的思维体系!

文章来源:智云一二三科技

文章标题:GoLang:OOP(面向对象)?坑

文章地址:https://www.zhihuclub.com/86411.shtml

关于作者: 智云科技

热门文章

网站地图