写在前面

因为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()
}
BaseBirdAdd()DerivedBirdAdd()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则同时有类和接口的概念,语法关键字分别为classinterface

/* 
* 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而言,虽然它有语法关键字structinterface,前者和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种特性如何来做呢?

封装

publicprotectedprivatepublicprivate

继承

implementsextends
type Base struct {
    // 字段
}
​
type Derived struct {
    Base, // 直接嵌入即可
}

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

多态

interface(接口)interfaceduck 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设计的理念中就没有怎么考虑OOPGoLang官方也声称不建议使用继承,鼓励多用组合。插一句题外话:继承多好啊!


III. GoLang的OOP:坑!

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

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

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

structBaseBirdCal()Add()DerivedBirdAdd()Cal()Add()BaseBird


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

interfaceinterface

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

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的简单语法+惯用思维做开发,这种实在可怕。希望这个踩过的“坑”能够帮助更多的人。