导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南。我们将以系列文章的形式推出《Go 开发的关键技术指南》,共有 4 篇文章,本文为第 3 篇。
Go 开发指南
Interfaces
Go 在类型和接口上的思考是:
- Go 类型系统并不是一般意义的 OO,并不支持虚函数;
- Go 的接口是隐含实现,更灵活,更便于适配和替换;
- Go 支持的是组合、小接口、组合+小接口;
- 接口设计应该考虑正交性,组合更利于正交性。
Type System
Go 的类型系统是比较容易和 C++/Java 混淆的,特别是习惯于类体系和虚函数的思路后,很容易想在 Go 走这个路子,可惜是走不通的。而 interface 因为太过于简单,而且和 C++/Java 中的概念差异不是特别明显,所以本章节专门分析 Go 的类型系统。
先看一个典型的问题 Is it possible to call overridden method from parent struct in golang? 代码如下所示:
package main
import (
"fmt"
)
type A struct {
}
func (a *A) Foo() {
fmt.Println("A.Foo()")
}
func (a *A) Bar() {
a.Foo()
}
type B struct {
A
}
func (b *B) Foo() {
fmt.Println("B.Foo()")
}
func main() {
b := B{A: A{}}
b.Bar()
}
本质上它是一个模板方法模式 (TemplateMethodPattern),A 的 Bar 调用了虚函数 Foo,期待子类重写虚函数 Foo,这是典型的 C++/Java 解决问题的思路。
我们借用模板方法模式 (TemplateMethodPattern) 中的例子,考虑实现一个跨平台编译器,提供给用户使用的函数是 crossCompile
,而这个函数调用了两个模板方法 collectSource
和 compileToTarget
:
public abstract class CrossCompiler {
public final void crossCompile() {
collectSource();
compileToTarget();
}
//Template methods
protected abstract void collectSource();
protected abstract void compileToTarget();
}
C 版,不用 OOAD 思维参考 C: CrossCompiler use StateMachine,代码如下所示:
// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>
void beforeCompile() {
printf("Before compile\n");
}
void afterCompile() {
printf("After compile\n");
}
void collectSource(bool isIPhone) {
if (isIPhone) {
printf("IPhone: Collect source\n");
} else {
printf("Android: Collect source\n");
}
}
void compileToTarget(bool isIPhone) {
if (isIPhone) {
printf("IPhone: Compile to target\n");
} else {
printf("Android: Compile to target\n");
}
}
void IDEBuild(bool isIPhone) {
beforeCompile();
collectSource(isIPhone);
compileToTarget(isIPhone);
afterCompile();
}
int main(int argc, char** argv) {
IDEBuild(true);
//IDEBuild(false);
return 0;
}
C 版本使用 OOAD 思维,可以参考 C: CrossCompiler,代码如下所示:
// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>
class CrossCompiler {
public:
void crossCompile() {
beforeCompile();
collectSource();
compileToTarget();
afterCompile();
}
private:
void beforeCompile() {
printf("Before compile\n");
}
void afterCompile() {
printf("After compile\n");
}
// Template methods.
public:
virtual void collectSource() = 0;
virtual void compileToTarget() = 0;
};
class IPhoneCompiler : public CrossCompiler {
public:
void collectSource() {
printf("IPhone: Collect source\n");
}
void compileToTarget() {
printf("IPhone: Compile to target\n");
}
};
class AndroidCompiler : public CrossCompiler {
public:
void collectSource() {
printf("Android: Collect source\n");
}
void compileToTarget() {
printf("Android: Compile to target\n");
}
};
void IDEBuild(CrossCompiler* compiler) {
compiler->crossCompile();
}
int main(int argc, char** argv) {
IDEBuild(new IPhoneCompiler());
//IDEBuild(new AndroidCompiler());
return 0;
}
我们可以针对不同的平台实现这个编译器,比如 Android 和 iPhone:
public class IPhoneCompiler extends CrossCompiler {
protected void collectSource() {
//anything specific to this class
}
protected void compileToTarget() {
//iphone specific compilation
}
}
public class AndroidCompiler extends CrossCompiler {
protected void collectSource() {
//anything specific to this class
}
protected void compileToTarget() {
//android specific compilation
}
}
在 C++/Java 中能够完美的工作,但是在 Go 中,使用结构体嵌套只能这么实现,让 IPhoneCompiler 和 AndroidCompiler 内嵌 CrossCompiler,参考 Go: TemplateMethod,代码如下所示:
package main
import (
"fmt"
)
type CrossCompiler struct {
}
func (v CrossCompiler) crossCompile() {
v.collectSource()
v.compileToTarget()
}
func (v CrossCompiler) collectSource() {
fmt.Println("CrossCompiler.collectSource")
}
func (v CrossCompiler) compileToTarget() {
fmt.Println("CrossCompiler.compileToTarget")
}
type IPhoneCompiler struct {
CrossCompiler
}
func (v IPhoneCompiler) collectSource() {
fmt.Println("IPhoneCompiler.collectSource")
}
func (v IPhoneCompiler) compileToTarget() {
fmt.Println("IPhoneCompiler.compileToTarget")
}
type AndroidCompiler struct {
CrossCompiler
}
func (v AndroidCompiler) collectSource() {
fmt.Println("AndroidCompiler.collectSource")
}
func (v AndroidCompiler) compileToTarget() {
fmt.Println("AndroidCompiler.compileToTarget")
}
func main() {
iPhone := IPhoneCompiler{}
iPhone.crossCompile()
}
执行结果却让人手足无措:
# Expect
IPhoneCompiler.collectSource
IPhoneCompiler.compileToTarget
# Output
CrossCompiler.collectSource
CrossCompiler.compileToTarget
Go 并没有支持类继承体系和多态,Go 是面向对象却不是一般所理解的那种面向对象,用老子的话说“道可道,非常道”。
实际上在 OOAD 中,除了类继承之外,还有另外一个解决问题的思路就是组合 Composition,面向对象设计原则中有个很重要的就是 The Composite Reuse Principle (CRP),Favor delegation over inheritance as a reuse mechanism
,重用机制应该优先使用组合(代理)而不是类继承。类继承会丧失灵活性,而且访问的范围比组合要大;组合有很高的灵活性,另外组合使用另外对象的接口,所以能获得最小的信息。
C++ 如何使用组合代替继承实现模板方法?可以考虑让 CrossCompiler 使用其他的类提供的服务,或者说使用接口,比如 CrossCompiler
依赖于 ICompiler
:
public interface ICompiler {
//Template methods
protected abstract void collectSource();
protected abstract void compileToTarget();
}
public abstract class CrossCompiler {
public ICompiler compiler;
public final void crossCompile() {
compiler.collectSource();
compiler.compileToTarget();
}
}
C 版本可以参考 C: CrossCompiler use Composition,代码如下所示:
// g++ compiler.cpp -o compiler && ./compiler
#include <stdio.h>
class ICompiler {
// Template methods.
public:
virtual void collectSource() = 0;
virtual void compileToTarget() = 0;
};
class CrossCompiler {
public:
CrossCompiler(ICompiler* compiler) : c(compiler) {
}
void crossCompile() {
beforeCompile();
c->collectSource();
c->compileToTarget();
afterCompile();
}
private:
void beforeCompile() {
printf("Before compile\n");
}
void afterCompile() {
printf("After compile\n");
}
ICompiler* c;
};
class IPhoneCompiler : public ICompiler {
public:
void collectSource() {
printf("IPhone: Collect source\n");
}
void compileToTarget() {
printf("IPhone: Compile to target\n");
}
};
class AndroidCompiler : public ICompiler {
public:
void collectSource() {
printf("Android: Collect source\n");
}
void compileToTarget() {
printf("Android: Compile to target\n");
}
};
void IDEBuild(CrossCompiler* compiler) {
compiler->crossCompile();
}
int main(int argc, char** argv) {
IDEBuild(new CrossCompiler(new IPhoneCompiler()));
//IDEBuild(new CrossCompiler(new AndroidCompiler()));
return 0;
}
我们可以针对不同的平台实现这个 ICompiler
,比如 Android 和 iPhone。这样从继承的类体系,变成了更灵活的接口的组合,以及对象直接服务的调用:
public class IPhoneCompiler implements ICompiler {
protected void collectSource() {
//anything specific to this class
}
protected void compileToTarget() {
//iphone specific compilation
}
}
public class AndroidCompiler implements ICompiler {
protected void collectSource() {
//anything specific to this class
}
protected void compileToTarget() {
//android specific compilation
}
}
在 Go 中,推荐用组合和接口,小的接口,大的对象。这样有利于只获得自己应该获取的信息,或者不会获得太多自己不需要的信息和函数,参考 Clients should not be forced to depend on methods they do not use. –Robert C. Martin,以及 The bigger the interface, the weaker the abstraction, Rob Pike。关于面向对象的原则在 Go 中的体现,参考 Go: SOLID 或中文版 Go: SOLID。
先看如何使用 Go 的思路实现前面的例子,跨平台编译器,Go Composition: Compiler,代码如下所示:
package main
import (
"fmt"
)
type SourceCollector interface {
collectSource()
}
type TargetCompiler interface {
compileToTarget()
}
type CrossCompiler struct {
collector SourceCollector
compiler TargetCompiler
}
func (v CrossCompiler) crossCompile() {
v.collector.collectSource()
v.compiler.compileToTarget()
}
type IPhoneCompiler struct {
}
func (v IPhoneCompiler) collectSource() {
fmt.Println("IPhoneCompiler.collectSource")
}
func (v IPhoneCompiler) compileToTarget() {
fmt.Println("IPhoneCompiler.compileToTarget")
}
type AndroidCompiler struct {
}
func (v AndroidCompiler) collectSource() {
fmt.Println("AndroidCompiler.collectSource")
}
func (v AndroidCompiler) compileToTarget() {
fmt.Println("AndroidCompiler.compileToTarget")
}
func main() {
iPhone := IPhoneCompiler{}
compiler := CrossCompiler{iPhone, iPhone}
compiler.crossCompile()
}
这个方案中,将两个模板方法定义成了两个接口,CrossCompiler
使用了这两个接口,因为本质上 C++/Java 将它的函数定义为抽象函数,意思也是不知道这个函数如何实现。而 IPhoneCompiler
和 AndroidCompiler
并没有继承关系,而它们两个实现了这两个接口,供 CrossCompiler
使用;也就是它们之间的关系,从之前的强制绑定,变成了组合。
type SourceCollector interface {
collectSource()
}
type TargetCompiler interface {
compileToTarget()
}
type CrossCompiler struct {
collector SourceCollector
compiler TargetCompiler
}
func (v CrossCompiler) crossCompile() {
v.collector.collectSource()
v.compiler.compileToTarget()
}
Rob Pike 在 Go Language: Small and implicit 中描述 Go 的类型和接口,第 29 页说:
- Objects implicitly satisfy interfaces. A type satisfies an interface simply by implementing its methods. There is no "implements" declaration; interfaces are satisfied implicitly. 这种隐式的实现接口,实际中还是很灵活的,我们在 Refector 时可以将对象改成接口,缩小所依赖的接口时,能够不改变其他地方的代码。比如如果一个函数
foo(f *os.File)
,最初依赖于os.File
,但实际上可能只是依赖于io.Reader
就可以方便做 UTest,那么可以直接修改成foo(r io.Reader)
所有地方都不用修改,特别是这个接口是新增的自定义接口时就更明显; - In Go, interfaces are usually small: one or two or even zero methods. 在 Go 中接口都比较小,非常小,只有一两个函数;但是对象却会比较大,会使用很多的接口。这种方式能够以最灵活的方式重用代码,而且保持接口的有效性和最小化,也就是接口隔离。
隐式实现接口有个很好的作用,就是两个类似的模块实现同样的服务时,可以无缝的提供服务,甚至可以同时提供服务。比如改进现有模块时,比如两个不同的算法。更厉害的时,两个模块创建的私有接口,如果它们签名一样,也是可以互通的,其实签名一样就是一样的接口,无所谓是不是私有的了。这个非常强大,可以允许不同的模块在不同的时刻升级,这对于提供服务的服务器太重要了。
比较被严重误认为是继承的,莫过于是 Go 的内嵌 Embeding,因为 Embeding 本质上还是组合不是继承,参考 Embeding is still composition。
Embeding 在 UTest 的 Mocking 中可以显著减少需要 Mock 的函数,比如 Mocking net.Conn,如果只需要 mock Read 和 Write 两个函数,就可以通过内嵌 net.Conn 来实现,这样 loopBack 也实现了整个 net.Conn 接口,不必每个接口全部写一遍:
type loopBack struct {
net.Conn
buf bytes.Buffer
}
func (c *loopBack) Read(b []byte) (int, error) {
return c.buf.Read(b)
}
func (c *loopBack) Write(b []byte) (int, error) {
return c.buf.Write(b)
}
Embeding 只是将内嵌的数据和函数自动全部代理了一遍而已,本质上还是使用这个内嵌对象的服务。Outer 内嵌了Inner,和 Outer 继承 Inner 的区别在于:内嵌 Inner 是不知道自己被内嵌,调用 Inner 的函数,并不会对 Outer 有任何影响,Outer 内嵌 Inner 只