golang int 转string_GoLang:OOP(面向对象)?坑!

GoLang 的面向对象编程与 C++ 和 JAVA 有所不同,不支持传统的继承,而是通过匿名组合实现类似效果。由于方法绑定的静态特性,导致在调用时不会动态绑定到子类的方法。解决这个问题可以利用接口实现多态。文章通过实例代码展示了 GoLang 中的封装、继承和多态,并提出在实际项目中如何避免遇到的坑。
摘要由CSDN通过智能技术生成

写在前面

因为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()
}

答案应该比较明显,BaseBirdAdd()是每次累加1;而DerivedBirdAdd()则是每次累加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则同时有类和接口的概念,语法关键字分别为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=%dn", this.name, this.age)
}
​
/* 
* interface 接口结构
*/
type Animal interface {
    Show()
    Eat()
}

II. GoLang:怎么做OOP?

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

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

封装

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

继承

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

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


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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值