Go 開發關鍵技術指南 | 帶著伺服器程式設計金剛經走進 2020 年(內含超全知識大圖)
點選這裡,檢視Orthogonal,Modules,GOPATH & Vendor等剩餘重要內容
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
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 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
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 只是自動將 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
,詳細參考正交性。
關鍵字:XML 儲存 Shell Go 開發工具 Android開發 iOS開發 資料格式 git C++
相關文章
- Go 開發關鍵技術指南 | 帶著伺服器程式設計金剛經走進 2020 年 (內含超全知識大圖)Go伺服器程式設計
- Go 開發關鍵技術指南 | Go 面向失敗程式設計 (內含超全知識大圖)Go程式設計
- Go 開發關鍵技術指南 | 敢問路在何方?(內含超全知識大圖)Go
- Go 開發關鍵技術指南 | 為什麼你要選擇 GO?(內含超全知識大圖)Go
- Go 開發關鍵技術指南 | 為什麼你要選擇 GO?(內含超全知識大圖)Go
- 一文讀懂分散式架構知識體系(內含超全核心知識大圖)分散式架構
- 剛剛,阿里開源600頁技術全景圖,看完少走10年彎路!阿里
- [知識分享] 租用高防伺服器防火牆的五大關鍵技術伺服器防火牆
- 如何熟悉一個系統?(內含知識大圖)
- 帶你走進零知識證明
- 技術分享:Linux多核並行程式設計關鍵技術Linux並行行程程式設計
- 雲原生時代|分散式系統設計知識圖譜(內含 22 個知識點)分散式
- OpenStack關鍵技術系列: Libvirt基礎知識
- 金武盟(NFT)系統程式設計開發技術(程式碼示例)程式設計
- Gangs Rabbit剛兔(NFT)系統程式設計開發示例(python技術示例)程式設計Python
- 雲原生時代,分散式系統設計必備知識圖譜(內含22個知識點)分散式
- Java開發程式設計師:JVM相關的知識講解Java程式設計師JVM
- 讓你成為前端,後端或全棧開發程式設計師的進階指南,一門學到老的技術前端後端全棧程式設計師
- 改進DevSecOps框架的 5 大關鍵技術dev框架
- 2020年Java程式設計師需要哪些技術Java程式設計師
- 帶你走進Choerodon豬齒魚的知識管理
- 走進JavaWeb技術世界1:JavaWeb的由來和基礎知識JavaWeb
- 知識圖譜關鍵技術與應用案例-CSDN公開課-專題視訊課程
- 再識Java併發程式設計關鍵字之volatileJava程式設計
- 寫給程式設計師---技術感悟及有關高併發伺服器框架設計程式設計師伺服器框架
- 如何畫好一張架構圖?(內含知識圖譜)架構
- CSS基本知識點——帶你走進CSS的新世界CSS
- WebGL程式設計指南(8)高階技術Web程式設計
- 好程式設計師雲端計算教程分享Mysql技術知識點程式設計師MySql
- Google I/O 2022: 促進知識和計算機技術發展Go計算機
- ☕【Java技術指南】「併發程式設計專題」針對於Guava RateLimiter限流器的入門到精通(含實戰開發技巧)Java程式設計GuavaMIT
- 【Go進階—併發程式設計】ContextGo程式設計Context
- 【Go進階—併發程式設計】WaitGroupGo程式設計AI
- 【Go進階—併發程式設計】MutexGo程式設計Mutex
- 好程式設計師Java教程分享Java技術知識點總結程式設計師Java
- JAVA程式設計師“黃金5年”必須要掌握的知識技能Java程式設計師
- 程式設計或者軟體開發到底算不算知識?程式設計
- java併發程式設計系列:java併發程式設計背景知識Java程式設計