语雀: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的具体实现。
好了,我们总结下使用接口的好处吧:
- 代码扩展性更强了。例如,同样的Bird,可以有不同的实现。在开发中用的更多的是,将数据库的CURD操作抽象成接口,从而可以实现同一份代码对接不同数据库的目的。
- 可以解耦上下游的实现。例如,LetItFly不用关注Bird是如何Fly的,只需要调用Bird提供的方法即可。
- 提高了代码的可测性。因为接口可以解耦上下游实现,我们在单元测试需要依赖第三方系统/数据库的代码时,可以利用接口将具体实现解耦,实现fake类型。
- 代码更健壮、更稳定了。例如,如果要更改Fly的方式,只需要更改相关类型的Fly方法即可,完全影响不到LetItFly函数。
所以,我建议你,在Go项目开发中,一定要多思考,那些可能有多种实现的地方,要考虑使用接口。
面向“对象”编程。
接下来,我们再来看下面向“对象”编程。
面向对象编程(OOP)有很多优点,例如可以使我们的代码变得易维护、易扩展,并能提高开发效率等,所以一个高质量的Go应用在需要时,也应该采用面向对象的方法去编程。那什么叫“在需要时”呢?就是我们在开发代码时,如果一个功能可以通过接近于日常生活和自然的思考方式来实现,这时候就应该考虑使用面向对象的编程方法。
Go语言不支持面向对象编程,但是却可以通过一些语言级的特性来实现类似的效果。
面向对象编程中,有几个核心特性:类、实例、抽象,封装、继承、多态、构造函数、析构函数、方法重载、this指针。在Go中可以通过以下几个方式来实现类似的效果:
- 类、抽象、封装通过结构体来实现。
- 实例通过结构体变量来实现。
- 继承通过组合来实现。这里解释下什么叫组合:一个结构体嵌到另一个结构体,称作组合。例如一个结构体包含了一个匿名结构体,就说这个结构体组合了该匿名结构体。
- 多态通过接口来实现。
至于构造函数、析构函数、方法重载和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类别的不同而返回不同的名字和类别,也就是说通过接口实现了多态。