这节不阐述OOP的理念,从接口直接讲,需要一定的抽象思想,新手可以绕道
go里面的接口定义
Go语言不同于其他语言。在Go语言中,接口是自定义类型,用于指定一组一个或多个方法签名,并且该接口是抽象的,因此不允许你创建该接口的实例。但是你可以创建接口类型的变量,并且可以为该变量分配一个具体的类型值,该值具有接口所需的方法。换句话说,接口既是方法的集合,也是自定义类型。
// 定义一个接口
type myinterface interface{
// 定义方法名
fun1() int
fun2() float64
}
我自己对接口的定义为接口是方法签名的集合。
struct里面可以定义方法,也可以定义成员变量,像我一直在做测试驱动开发的事情,在我眼里,一个好的设计就是对象本身定义了必需的内部元素,接口则定义了对外表示的行为规范,按照这样的设计才能真正的做到解耦。
在Go语言中,必须实现在接口中声明的所有方法以实现接口。go语言接口是隐式实现的,官方是叫非侵入式接口。并且它不包含任何其他关键字来实现与其他语言一样的接口。
这点和我以前写c#和java的时候差别很大,以前创建一个接口实体类,必须显式声明。
public class Instance implements InterfaceA, InterfaceB {
// ...
}
区别在哪里呢?
在我看来,最大的区别在于,引用包的定义可以减少,这样不同包不用因为满足接口的显示定义而去修改。很难理解?
我解释下,在c#和java里面,你去引用一个接口的实现类,你必须显式引用该接口的namespace,而这个在go里面是有个问题的,go的package引用其实本质上更像是c语言的头文件的升级版融合,并没有namespace的概念,比如java和c#同一个namespace里面的多个包可以互相引用而不需要考虑循环报错的问题,而c语言里这个是非常严重的错误,说明你模块分层的还不够有上下引用的规范。必须强制重组一部分代码才能过编译。
我在用go主导大项目的时候前期经常遇见这个问题,而有时候两个模块之间相互调用是天然需要的,这不是设计的问题,那如何解耦,这个时候就必须要用接口了。
这里和java与c#之类是一样的做法,我传过来的对象不会显式声明它的实际类型,而是声明它的接口类型即可。这样,就不需要引用实体类的定义包了,这个其实就是面向对象接口或者说IOC的核心体现。
回过来说,为什么引用包的定义可以减少,我们在定义一个大项目的模块的时候,经常把interface/model/instance的定义分开,interface按照职责分成几个部分单独定义,对象的实现和interface的定义是需要分开的。
这个时候,我们的实现类可以不需要引用interface的包就可以已经实现了该interface,这样会更简洁,断了表面的联系,一身轻。
接口检查
那么go没有显示声明基于哪个接口实现,运行的时候不会出问题吗?
这个要分情况,一种是我们在定义一个接口变量等于一个实体变量的时候,编译器会自动做检查,这个是在静态编译期间就实现的,后面运行的抽象也是无成本的。
type myInterface interface {
SayFunc() int
}
type myInstance struct {
}
func (thiz *myInstance )SayFunc() int{
....
}
var obj myInterface
obj = new(myInstance)//这步编译器会做接口规范是否符合的检查
所以静态编译时期不用担心,接口的方法定义go/java都强制了代码被封装。
还有一个就是运行时用类型断言来检查,go是支持类型断言的,运行时断言如果不过,则可以返回失败或者panic
面向接口编程
理念这个东西很抽象,从实际来讲,为什么要面向接口编程。
笔者从嵌入式C一直往上做到云计算这块,越往上层走,面向对象的水平要求就越高,但是我认为,面向对象的核心就是面向接口编程,因为接口是解耦的核心。
像我们做过java开发的就知道,java在工程类项目上真的是生态无敌,简单来说,就spring一个足矣。
spring的核心是依赖注入和面向切面编程。
而这两个的核心依然是基于IOC的,IOC控制反转的核心则是接口化的对象。
现在的spring依赖注入是直接在内部定义的接口成员变量上的标注上写它的实例在哪。
以前我更多的是在xml的配置文件里面做好这个映射关系的,效果都是一样的,现在继承在变量标注定义里面更简洁了,当然了,核心是java的反射太强大了,spring的源码里面是在package里面把所有类定义全部遍历出来,然后里面的成员变量看下有没有实现对应的标注,这个时候把映射关系做好。
这点我一直想自己实现一个go的spring框架,后来找了很多,自己也踩了很多坑,发现没法做,原因还是前面说的go的package和Java的不一样,原因有两点。
- go的反射不支持凭空根据一个packagePath.structName来实例化一个对象出来,java可以。
- go的反射无法在package包里列出包含的struct集合,java可以。
go的理念是大道至简,没有泛型,没有try,而且根据我对go的设计里面的理解,这两个长期来看,依然不会支持。那既然反射能力只有这么弱,那抱怨也是没办法的,我就找找github上的依赖注入的实现吧。
后来找了很多,发现还是无法满足我的大项目的需求,比如我贴一下google官方推的一个库 go_wire
type Message string
type Greeter struct {
// ... TBD
}
type Event struct {
// ... TBD
}
func NewMessage() Message {
return Message("Hi there!")
}
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
type Greeter struct {
Message Message // <- adding a Message field
}
func (g Greeter) Greet() Message {
return g.Message
}
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
type Event struct {
Greeter Greeter // <- adding a Greeter field
}
// wire.go
func InitializeEvent() Event {
wire.Build(NewEvent, NewGreeter, NewMessage)//核心逻辑都在里面
return Event{}
}
// wire_gen.go
/*func InitializeEvent() Event {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
return event
}*/
func main() {
e := InitializeEvent()
e.Start()
}
看似一个依赖注入实现了,但是这个框架本身并不能满足绝大部分项目需求,原因如下:
- 这个框架在用NewXXX()这些方法的时候显示的引用了实例,并没有在引用层面解耦,只是在调用方法封装的更好看一些而已
- wire.Build的源码我看了,内部逻辑是根据需要的目标接口递归的遍历NewXXX()方法的返回对象定义看哪个满足接口的定义,满足则调用方法返回一个对象,不满足则递归往New里继续搜索。这个会有个问题就是,接口的实例只能唯一类,如果我要的是现在普遍的根据一个接口类型+对象名,我就要返回我要的实例,这个框架无能为力。
- 有人说第2点可我可以定义多个这样的实例provider方法来解决啊,同时会有两个问题出来,一是写的很麻烦,经常要修改代码,特别繁琐,二是这个定义去拿一个接口往往变成了运行中间频繁去调用,反射极其消耗性能,一个provider方法去拿一个实例根据我的测试少说也得5000ns,对性能影响极大。
那假如我在做一个大型项目,我已经建模抽象化了很多模块,一个接口的实现就有N多实例,我如何提高这个项目的健壮性和可维护性呢,这个可以继续看我的下篇文章 go依赖注入的实现