写在前面
因为GoLang开发效率匹配Python,而性能可以接近C++,仅仅这两大特点就使得GoLang很快站稳了脚跟,并且使用率和占有率逐步攀升。然而在在实际项目中使用GoLang的时候,还是需要当心!本文就来讲一讲笔者在使用GoLang做面向对象的时候遇到的坑。
本文的代码篇幅会比较多,但是代码绝!对!不!复!杂!
首先,我们来看一看下面的代码,请问运行的结果是什么呢?
package main
import "fmt"
type BaseBird struct {
age int
}
func (this *BaseBird)Add() {
fmt.Printf("before add: age=%dn", this.age)
this.age = this.age + 1
fmt.Printf("after add: age=%dn", this.age)
}
type DerivedBird struct {
BaseBird
}
func (this *DerivedBird) Add() {
fmt.Printf("before add: age=%dn", this.age)
this.age = this.age + 2
fmt.Printf("after add: age=%dn", 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=%dn", this.age)
this.age = this.age + 1
fmt.Printf("after add: age=%dn", this.age)
}
type DerivedBird struct {
BaseBird
}
func (this *DerivedBird) Add() {
fmt.Printf("before add: age=%dn", this.age)
this.age = this.age + 2
fmt.Printf("after add: age=%dn", 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=%dn", 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=%dn", 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=%dn", 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=%dn", this.name, this.age)
}
type Dog struct {
name string
age int
}
func (this *Dog) Show() {
fmt.Printf("Dog: name=%s, age=%dn", 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=%dn", age);
age += 1;
printf("after add, age=%dn", age);
};
};
class DerivedBird : public BaseBird {
public:
DerivedBird(int _age) : BaseBird(_age) {};
~DerivedBird() = default;
void Add() {
printf("before add, age=%dn", age);
age += 2;
printf("after add, age=%dn", 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=%dn", this.age)
this.age = this.age + 1
fmt.Printf("after add: age=%dn", this.age)
}
type DerivedBird struct {
BaseBird
}
func (this *DerivedBird) Add() {
fmt.Printf("before add: age=%dn", this.age)
this.age = this.age + 2
fmt.Printf("after add: age=%dn", 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的简单语法+惯用思维做开发,这种实在可怕。希望这个踩过的“坑”能够帮助更多的人。