注“阿里巴巴云原生”公众号,回复 Go 即可查看清晰知识大图!
导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南。我们将以系列文章的形式推出《Go 开发的关键技术指南》,共有 4 篇文章,本文为第 3 篇。
作者 | 杨成立(忘篱) 阿里巴巴高级技术专家
导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南。我们将以系列文章的形式推出《Go 开发的关键技术指南》,共有 4 篇文章,本文为第 3 篇。
Go 开发指南Go 在类型和接口上的思考是:
- Go 类型系统并不是一般意义的 OO,并不支持虚函数;
- Go 的接口是隐含实现,更灵活,更便于适配和替换;
- Go 支持的是组合、小接口、组合+小接口;
- 接口设计应该考虑正交性,组合更利于正交性。
Go 的类型系统是比较容易和 C++/Java 混淆的,特别是习惯于类体系和虚函数的思路后,很容易想在 Go 走这个路子,可惜是走不通的。而 interface 因为太过于简单,而且和 C++/Java 中的概念差异不是特别明显,所以本章节专门分析 Go 的类型系统。
先看一个典型的问题 Is it possible to call overridden method from parent struct in golang? 代码如下所示:
package mainimport ( "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 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 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 mainimport ( "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()}
执行结果却让人手足无措:
# ExpectIPhoneCompiler.collectSourceIPhoneCompiler.compileToTarget# OutputCrossCompiler.collectSourceCrossCompiler.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 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 mainimport ( "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 只是自动将 Inner 的数据和方法代理了一遍,但是本质上 Inner 的东西还不是 Outer 的东西;对于继承,调用 Inner 的函数有可能会改变 Outer 的数据,因为 Outer 继承 Inner,那么 Outer 就是 Inner,二者的依赖是更紧密的。
如果很难理解为何 Embeding 不是继承,本质上是没有区分继承和组合的区别,可以参考 Composition not inheritance,Go 选择组合不选择继承是深思熟虑的决定,面向对象的继承、虚函数、多态和类树被过度使用了。类继承树需要前期就设计好,而往往系统在演化时发现类继承树需要变更,我们无法在前期就精确设计出完美的类继承树;Go 的接口和组合,在接口变更时,只需要变更最直接的调用层,而没有类子树需要变更。
The designs are nothing like hierarchical, subtype-inherited methods. They are looser (even ad hoc), organic, decoupled, independent, and therefore scalable.
组合比继承有个很关键的优势是正交性 orthogonal
,详细参考正交性。
真水无香,真的牛逼不用装。——来自网络
软件是一门科学也是艺术,换句话说软件是工程。科学的意思是逻辑、数学、二进制,比较偏基础的理论都是需要数学的,比如 C 的结构化编程是有论证的,那些关键字和逻辑是够用的。实际上 Go 的 GC 也是有数学证明的,还有一些网络传输算法,又比如奠定一个新领域的论文比如 Google 的论文。艺术的意思是,大部分时候都用不到严密的论证,有很多种不同的路,还需要看自己的品味或者叫偏见,特别容易引起口水仗和争论,从好的方面说,好的软件或代码,是能被感觉到很好的。
由于大部分时候软件开发是要靠经验的,特别是国内填鸭式教育培养了对于数学的莫名的仇恨(“莫名”主要是早就把该忘的不该忘记的都忘记了),所以在代码中强调数学,会激发起大家心中一种特别的鄙视和怀疑,而这种鄙视和怀疑应该是以葱白和畏惧为基础——大部分时候在代码中吹数学都会被认为是装逼。而 Orthogonal (正交性)则不择不扣的是个数学术语,是线性代数(就是矩阵那个玩意儿)中用来描述两个向量相关性的,在平面中就是两个线条的垂直。比如下图:
Vectors A and B are orthogonal to each other.
旁白:妮玛,两个线条垂直能和代码有个毛线关系,八竿子打不着关系吧,请继续吹。
先请看 Go 关于 Orthogonal 相关的描述,可能还不止这些地方:
Composition not inheritance Object-oriented programming provides a powerful insight: that the behavior of data can be generalized independently of the representation of that data. The model works best when the behavior (method set) is fixed, but once you subclass a type and add a method, the behaviors are no longer identical. If instead the set of behaviors is fixed, such as in Go's statically defined interfaces, the uniformity of behavior enables data and programs to be composed uniformly, orthogonally, and safely.
JSON-RPC: a tale of interfaces In an inheritance-oriented language like Java or C++, the obvious path would be to generalize the RPC class, and create JsonRPC and GobRPC subclasses. However, this approach becomes tricky if you want to make a further generalization orthogonal to that hierarchy.
实际上 Orthogonal 并不是只有 Go 才提,参考 Orthogonal Software。实际上很多软件设计都会提正交性,比如 OOAD 里面也有不少地方用这个描述。我们先从实际的例子出发吧,关于线程一般 Java、Python、C# 等语言,会定义个线程的类 Thread,可能包含以下的方法管理线程:
var thread = new Thread(thread_main_function);thread.Start();thread.Interrupt();thread.Join();thread.Stop();
如果把 goroutine 也看成是 Go 的线程,那么实际上 Go 并没有提供上面的方法,而是提供了几种不同的机制来管理线程:
go
关键键字启动 goroutine;sync.WaitGroup
等待线程退出;chan
也可以用来同步,比如等 goroutine 启动或退出,或者传递退出信息给 goroutine;context
也可以用来管理 goroutine,参考 Context。
s := make(chan bool, 0)q := make(chan bool, 0)go func() { s
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?