因为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的思维体系!