Docs Vault

语雀:https://www.yuque.com/konglingfei-vzag4/onex/zghg3im31hv79qb4


编程哲学,其实就是要编写符合Go语言设计哲学的代码。Go语言有很多设计哲学,对代码质量影响比较大的,我认为有两个:面向接口编程和面向"对象"编程。


面向接口编程


我们先来看下面向接口编程。


Go 接口是一组方法的集合。任何类型,只要实现了该接口中的方法集,那么就属于这个类型,也称为实现了该接口。


有很多优秀的开源项目,都大量采用了面向接口的边方式,例如:Kubernetes、Docker、Prometheus 等。

接口的作用,其实就是为不同层级的模块提供一个定义好的中间层。这样,上游不再需要依赖下游的具体实现,充分地对上下游进行了解耦。很多流行的Go设计模式,就是通过面向接口编程的思想来实现的。


我们看一个面向接口编程的例子。下面这段代码定义了一个Bird接口,Canary和Crow类型均实现了Bird接口。

package main


import "fmt"


// 定义了一个鸟类
type Bird interface {
    Fly()
    Type() string
}


// 鸟类:金丝雀
type Canary struct {
    Name string
}


func (c *Canary) Fly() {
    fmt.Printf("我是%s,用黄色的翅膀飞\n", c.Name)
}
func (c *Canary) Type() string {
    return c.Name
}


// 鸟类:乌鸦
type Crow struct {
    Name string
}


func (c *Crow) Fly() {
    fmt.Printf("我是%s,我用黑色的翅膀飞\n", c.Name)
}


func (c *Crow) Type() string {
    return c.Name
}


// 让鸟类飞一下
func LetItFly(bird Bird) {
    fmt.Printf("Let %s Fly!\n", bird.Type())
    bird.Fly()
}


func main() {
    LetItFly(&Canary{"金丝雀"})
    LetItFly(&Crow{"乌鸦"})
}


这段代码中,因为Crow和Canary都实现了Bird接口声明的Fly、Type方法,所以可以说Crow、Canary实现了Bird接口,属于Bird类型。在函数调用时,可以传入Bird类型,并在函数内部调用Bird接口提供的方法,以此来解耦Bird的具体实现。


好了,我们总结下使用接口的好处吧:

  1. 代码扩展性更强了。例如,同样的Bird,可以有不同的实现。在开发中用的更多的是,将数据库的CURD操作抽象成接口,从而可以实现同一份代码对接不同数据库的目的。
  2. 可以解耦上下游的实现。例如,LetItFly不用关注Bird是如何Fly的,只需要调用Bird提供的方法即可。
  3. 提高了代码的可测性。因为接口可以解耦上下游实现,我们在单元测试需要依赖第三方系统/数据库的代码时,可以利用接口将具体实现解耦,实现fake类型。
  4. 代码更健壮、更稳定了。例如,如果要更改Fly的方式,只需要更改相关类型的Fly方法即可,完全影响不到LetItFly函数。


所以,我建议你,在Go项目开发中,一定要多思考,那些可能有多种实现的地方,要考虑使用接口。


面向“对象”编程。


接下来,我们再来看下面向“对象”编程。


面向对象编程(OOP)有很多优点,例如可以使我们的代码变得易维护、易扩展,并能提高开发效率等,所以一个高质量的Go应用在需要时,也应该采用面向对象的方法去编程。那什么叫“在需要时”呢?就是我们在开发代码时,如果一个功能可以通过接近于日常生活和自然的思考方式来实现,这时候就应该考虑使用面向对象的编程方法。


Go语言不支持面向对象编程,但是却可以通过一些语言级的特性来实现类似的效果。


面向对象编程中,有几个核心特性:类、实例、抽象,封装、继承、多态、构造函数、析构函数、方法重载、this指针。在Go中可以通过以下几个方式来实现类似的效果:

  1. 类、抽象、封装通过结构体来实现。
  2. 实例通过结构体变量来实现。
  3. 继承通过组合来实现。这里解释下什么叫组合:一个结构体嵌到另一个结构体,称作组合。例如一个结构体包含了一个匿名结构体,就说这个结构体组合了该匿名结构体。
  4. 多态通过接口来实现。


至于构造函数、析构函数、方法重载和this指针等,Go为了保持语言的简洁性去掉了这些特性。


Go中面向对象编程方法,见下图:


我们通过一个示例,来具体看下Go是如何实现面向对象编程中的类、抽象、封装、继承和多态的。代码如下:


package main


import "fmt"


// 基类:Bird
type Bird struct {
    Type string
}


// 鸟的类别
func (bird *Bird) Class() string {
    return bird.Type
}


// 定义了一个鸟类
type Birds interface {
    Name() string
    Class() string
}


// 鸟类:金丝雀
type Canary struct {
    Bird
    name string
}


func (c *Canary) Name() string {
    return c.name
}


// 鸟类:乌鸦
type Crow struct {
    Bird
    name string
}


func (c *Crow) Name() string {
    return c.name
}


func NewCrow(name string) *Crow {
    return &Crow{
        Bird: Bird{
            Type: "Crow",
        },
        name: name,
    }
}


func NewCanary(name string) *Canary {
    return &Canary{
        Bird: Bird{
            Type: "Canary",
        },
        name: name,
    }
}


func BirdInfo(birds Birds) {
    fmt.Printf("I'm %s, I belong to %s bird class!\n", birds.Name(), birds.Class())
}


func main() {
    canary := NewCanary("CanaryA")
    crow := NewCrow("CrowA")
    BirdInfo(canary)
    BirdInfo(crow)
}


将上述代码保存在 oop.go 文件中,执行以下代码输出如下:


$ go run oop.go
I'm CanaryA, I belong to Canary bird class!
I'm CrowA, I belong to Crow bird class!


在上面的例子中,分别通过Canary和Crow结构体定义了金丝雀和乌鸦两种类别的鸟,其中分别封装了name属性和Name方法。也就是说通过结构体实现了类,该类抽象了鸟类,并封装了该鸟类的属性和方法。


在Canary和Crow结构体中,都有一个Bird匿名字段,Bird字段为Canary和Crow类的父类,Canary和Crow继承了Bird类的Class属性和方法。也就是说通过匿名字段实现了继承。


在main函数中,通过NewCanary创建了Canary鸟类实例,并将其传给BirdInfo函数。也就是说通过结构体变量实现实例。


在BirdInfo函数中,将Birds接口类型作为参数传入,并在函数中调用了birds.Name,birds.Class方法,这两个方法会根据birds类别的不同而返回不同的名字和类别,也就是说通过接口实现了多态。