詳解Go語(yǔ)言中接口應(yīng)用模式或慣例介紹
一、前置原則
在了解接口應(yīng)用模式之前,我們還先要了解一個(gè)前置原則,那就是在實(shí)際真正需要的時(shí)候才對(duì)程序進(jìn)行抽象。再通俗一些來(lái)講,就是不要為了抽象而抽象。接口本質(zhì)上是一種抽象,它的功能是解耦,所以這條原則也在告訴我們:不要為了使用接口而使用接口。舉一個(gè)簡(jiǎn)單的例子,如果我們要給一個(gè)計(jì)算器添加一個(gè)整數(shù)加法的功能特性,本來(lái)一個(gè)函數(shù)就可以實(shí)現(xiàn):
func Add(a int64, b int64) int64 { return a+b }
但如果你非要引入一個(gè)接口,結(jié)果代碼可能就變成了這樣:
type Adder interface { Add(int64, int64) int64 } func Add(adder Adder, a int64, b int64) int64 { return adder.Add(a, b) }
這就會(huì)產(chǎn)生一種“過(guò)設(shè)計(jì)”的味道了。
要注意,接口的確可以實(shí)現(xiàn)解耦,但它也會(huì)引入“抽象”的副作用,或者說(shuō)接口這種抽象也不是免費(fèi)的,是有成本的,除了會(huì)造成運(yùn)行效率的下降之外,也會(huì)影響代碼的可讀性。不過(guò)這里你就不要拿我之前講解中的實(shí)戰(zhàn)例子去對(duì)號(hào)入座了,那些例子更多是為了讓你學(xué)習(xí) Go 語(yǔ)法的便利而構(gòu)建的。
在多數(shù)情況下,在真實(shí)的生產(chǎn)項(xiàng)目中,接口都能給應(yīng)用設(shè)計(jì)帶來(lái)好處。那么如果要用接口,我們應(yīng)該怎么用呢?怎么借助接口來(lái)改善程序的設(shè)計(jì),讓系統(tǒng)實(shí)現(xiàn)我們常說(shuō)的高內(nèi)聚和低耦合呢?這就要從 Go 語(yǔ)言的“組合”的設(shè)計(jì)哲學(xué)說(shuō)起。
二、一切皆組合
2.1 一切皆組合
Go
語(yǔ)言之父 Rob Pike 曾說(shuō)過(guò):如果 C++
和 Java
是關(guān)于類型層次結(jié)構(gòu)和類型分類的語(yǔ)言,那么 Go
則是關(guān)于組合的語(yǔ)言。如果把 Go
應(yīng)用程序比作是一臺(tái)機(jī)器的話,那么組合關(guān)注的就是如何將散落在各個(gè)包中的“零件”關(guān)聯(lián)并組裝到一起。組合是 Go
語(yǔ)言的重要設(shè)計(jì)哲學(xué)之一,而正交性則為組合哲學(xué)的落地提供了更為方便的條件。
正交(Orthogonality
)是從幾何學(xué)中借用的術(shù)語(yǔ),說(shuō)的是如果兩條線以直角相交,那么這兩條線就是正交的,比如我們?cè)诖鷶?shù)課程中經(jīng)常用到的坐標(biāo)軸就是這樣。用向量術(shù)語(yǔ)說(shuō),這兩條直線互不依賴,沿著某一條直線移動(dòng),你投影到另一條直線上的位置不變。
在計(jì)算機(jī)技術(shù)中,正交性用于表示某種不相依賴性或是解耦性。如果兩個(gè)或更多事物中的一個(gè)發(fā)生變化,不會(huì)影響其他事物,那么這些事物就是正交的。比如,在設(shè)計(jì)良好的系統(tǒng)中,數(shù)據(jù)庫(kù)代碼與用戶界面是正交的:你可以改動(dòng)界面,而不影響數(shù)據(jù)庫(kù);更換數(shù)據(jù)庫(kù),而不用改動(dòng)界面。
編程語(yǔ)言的語(yǔ)法元素間和語(yǔ)言特性也存在著正交的情況,并且通過(guò)將這些正交的特性組合起來(lái),我們可以實(shí)現(xiàn)更為高級(jí)的特性。在語(yǔ)言設(shè)計(jì)層面,Go 語(yǔ)言就為廣大 Gopher 提供了諸多正交的語(yǔ)法元素供后續(xù)組合使用,包括:
- Go 語(yǔ)言無(wú)類型體系(
Type Hierarchy
),沒(méi)有父子類的概念,類型定義是正交獨(dú)立的; - 方法和類型是正交的,每種類型都可以擁有自己的方法集合,方法本質(zhì)上只是一個(gè)將
receiver
參數(shù)作為第一個(gè)參數(shù)的函數(shù)而已; - 接口與它的實(shí)現(xiàn)者之間無(wú)“顯式關(guān)聯(lián)”,也就說(shuō)接口與 Go 語(yǔ)言其他部分也是正交的。
在這些正交語(yǔ)法元素當(dāng)中,接口作為 Go 語(yǔ)言提供的具有天然正交性的語(yǔ)法元素,在 Go 程序的靜態(tài)結(jié)構(gòu)搭建與耦合設(shè)計(jì)中扮演著至關(guān)重要的角色。 而要想知道接口究竟扮演什么角色,我們就先要了解組合的方式。
構(gòu)建 Go 應(yīng)用程序的靜態(tài)骨架結(jié)構(gòu)有兩種主要的組合方式,如下圖所示:
我們看到,這兩種組合方式分別為垂直組合和水平組合,那這兩種組合的各自含義與應(yīng)用范圍是什么呢?下面我們分別看看這兩種組合。
2.2 垂直組合
垂直組合更多用在將多個(gè)類型(如上圖中的 T1
、I1
等)通過(guò)“類型嵌入(Type Embedding
)”的方式實(shí)現(xiàn)新類型(如 NT1
)的定義。
傳統(tǒng)面向?qū)ο缶幊陶Z(yǔ)言(比如:C++
)大多是通過(guò)繼承的方式建構(gòu)出自己的類型體系的,但 Go
語(yǔ)言并沒(méi)有類型體系的概念。Go
語(yǔ)言通過(guò)類型的組合而不是繼承讓單一類型承載更多的功能。由于這種方式與硬件配置升級(jí)的垂直擴(kuò)展很類似,所以這里我們叫它垂直組合。
又因?yàn)椴皇抢^承,那么通過(guò)垂直組合定義的新類型與被嵌入的類型之間就沒(méi)有所謂“父子關(guān)系”的概念了,也沒(méi)有向上、向下轉(zhuǎn)型(Type Casting
),被嵌入的類型也不知道將其嵌入的外部類型的存在。調(diào)用方法時(shí),方法的匹配取決于方法名字,而不是類型。
這樣的垂直組合更多應(yīng)用在新類型的定義方面。通過(guò)這種垂直組合,我們可以達(dá)到方法實(shí)現(xiàn)的復(fù)用、接口定義重用等目的。
在實(shí)現(xiàn)層面,Go 語(yǔ)言通過(guò)類型嵌入(Type Embedding
)實(shí)現(xiàn)垂直組合,組合方式主要有以下幾種。
2.2.1 第一種:通過(guò)嵌入接口構(gòu)建接口
通過(guò)在接口定義中嵌入其他接口類型,實(shí)現(xiàn)接口行為聚合,組成大接口。這種方式在標(biāo)準(zhǔn)庫(kù)中非常常見(jiàn),也是 Go 接口類型定義的慣例。
比如這個(gè) ReadWriter
接口類型就采用了這種類型嵌入方式:
// $GOROOT/src/io/io.go type ReadWriter interface { Reader Writer }
2.2.2 第二種:通過(guò)嵌入接口構(gòu)建結(jié)構(gòu)體類型
這里我們直接來(lái)看一個(gè)通過(guò)嵌入接口類型創(chuàng)建新結(jié)構(gòu)體類型的例子:
type MyReader struct { io.Reader // underlying reader N int64 // max bytes remaining }
在結(jié)構(gòu)體中嵌入接口,可以用于快速構(gòu)建滿足某一個(gè)接口的結(jié)構(gòu)體類型,來(lái)滿足某單元測(cè)試的需要,之后我們只需要實(shí)現(xiàn)少數(shù)需要的接口方法就可以了。尤其是將這樣的結(jié)構(gòu)體類型變量傳遞賦值給大接口的時(shí)候,就更能體現(xiàn)嵌入接口類型的優(yōu)勢(shì)了。
2.2.3 第三種:通過(guò)嵌入結(jié)構(gòu)體類型構(gòu)建新結(jié)構(gòu)體類型
在結(jié)構(gòu)體中嵌入接口類型名和在結(jié)構(gòu)體中嵌入其他結(jié)構(gòu)體,都是“委派模式(delegate
)”的一種應(yīng)用。對(duì)新結(jié)構(gòu)體類型的方法調(diào)用,可能會(huì)被“委派”給該結(jié)構(gòu)體內(nèi)部嵌入的結(jié)構(gòu)體的實(shí)例,通過(guò)這種方式構(gòu)建的新結(jié)構(gòu)體類型就“繼承”了被嵌入的結(jié)構(gòu)體的方法的實(shí)現(xiàn)。
現(xiàn)在我們可以知道,包括嵌入接口類型在內(nèi)的各種垂直組合更多用于類型定義層面,本質(zhì)上它是一種類型組合,也是一種類型之間的耦合方式。
接著,我們來(lái)看看水平組合。
2.3 水平組合
當(dāng)我們通過(guò)垂直組合將一個(gè)個(gè)類型建立完畢后,就好比我們已經(jīng)建立了整個(gè)應(yīng)用程序骨架中的“器官”,那這些器官手、手臂等,那么這些“器官”之間又是通過(guò)關(guān)節(jié)連接在一起的。
在 Go 應(yīng)用靜態(tài)骨架中,什么元素經(jīng)常扮演著“關(guān)節(jié)”的角色呢?我們先來(lái)看個(gè)例子,假設(shè)現(xiàn)在我們有一個(gè)任務(wù),要編寫(xiě)一個(gè)函數(shù),實(shí)現(xiàn)將一段數(shù)據(jù)寫(xiě)入磁盤(pán)的功能。通常我們都可以很容易地寫(xiě)出下面的函數(shù):
func Save(f *os.File, data []byte) error
我們看到,這個(gè)函數(shù)使用一個(gè) *os.File
來(lái)表示數(shù)據(jù)寫(xiě)入的目的地,這個(gè)函數(shù)實(shí)現(xiàn)后可以工作得很好。但這里依舊存在一些問(wèn)題,我們來(lái)看一下。
首先,這個(gè)函數(shù)很難測(cè)試。os.File
是一個(gè)封裝了磁盤(pán)文件描述符(又稱句柄)的結(jié)構(gòu)體,只有通過(guò)打開(kāi)或創(chuàng)建真實(shí)磁盤(pán)文件才能獲得這個(gè)結(jié)構(gòu)體的實(shí)例,這就意味著,如果我們要對(duì) Save
這個(gè)函數(shù)進(jìn)行單元測(cè)試,就必須使用真實(shí)的磁盤(pán)文件。測(cè)試過(guò)程中,通過(guò) Save
函數(shù)寫(xiě)入文件后,我們還需要再次操作文件、讀取剛剛寫(xiě)入的內(nèi)容來(lái)判斷寫(xiě)入內(nèi)容是否正確,并且每次測(cè)試結(jié)束前都要對(duì)創(chuàng)建的臨時(shí)文件進(jìn)行清理,避免給后續(xù)的測(cè)試帶去影響。
其次,Save
函數(shù)違背了接口分離原則。根據(jù)業(yè)界廣泛推崇的 Robert Martin(Bob 大叔)的接口分離原則(ISP 原則,Interface Segregation Principle),也就是客戶端不應(yīng)該被迫依賴他們不使用的方法,我們會(huì)發(fā)現(xiàn) os.File
不僅包含 Save
函數(shù)需要的與寫(xiě)數(shù)據(jù)相關(guān)的 Write
方法,還包含了其他與保存數(shù)據(jù)到文件操作不相關(guān)的方法。比如,你也可以看下 *os.File
包含的這些方法:
func (f *File) Chdir() error func (f *File) Chmod(mode FileMode) error func (f *File) Chown(uid, gid int) error ... ...
這種讓 Save
函數(shù)被迫依賴它所不使用的方法的設(shè)計(jì)違反了 ISP 原則。
最后,Save
函數(shù)對(duì) os.File
的強(qiáng)依賴讓它失去了擴(kuò)展性。像 Save
這樣的功能函數(shù),它日后很大可能會(huì)增加向網(wǎng)絡(luò)存儲(chǔ)寫(xiě)入數(shù)據(jù)的功能需求。但如果到那時(shí)我們?cè)賮?lái)改變 Save
函數(shù)的函數(shù)簽名(參數(shù)列表 + 返回值)的話,將影響到 Save
函數(shù)的所有調(diào)用者。
綜合考慮這幾種原因,我們發(fā)現(xiàn) Save
函數(shù)所在的“器官”與 os.File
所在的“器官”之間采用了一種硬連接的方式,而以 os.File
這樣的結(jié)構(gòu)體作為“關(guān)節(jié)”讓它連接的兩個(gè)“器官”喪失了相互運(yùn)動(dòng)的自由度,讓它與它連接的兩個(gè)“器官”構(gòu)成的聯(lián)結(jié)體變得“僵直”。
那么,我們應(yīng)該如何更換“關(guān)節(jié)”來(lái)改善 Save
的設(shè)計(jì)呢?我們來(lái)試試接口。新版的 Save
函數(shù)原型如下:
func Save(w io.Writer, data []byte) error
可以看到,我們用 io.Writer
接口類型替換掉了 *os.File
。這樣一來(lái),新版 Save 的設(shè)計(jì)就符合了接口分離原則,因?yàn)?nbsp;io.Writer
僅包含一個(gè) Write
方法,而且這個(gè)方法恰恰是 Save 唯一需要的方法。
另外,這里我們以 io.Writer
接口類型表示數(shù)據(jù)寫(xiě)入的目的地,既可以支持向磁盤(pán)寫(xiě)入,也可以支持向網(wǎng)絡(luò)存儲(chǔ)寫(xiě)入,并支持任何實(shí)現(xiàn)了 Write
方法的寫(xiě)入行為,這讓 Save
函數(shù)的擴(kuò)展性得到了質(zhì)的提升。
還有一點(diǎn),也是之前我們一直強(qiáng)調(diào)的,接口本質(zhì)是契約,具有天然的降低耦合的作用。基于這點(diǎn),我們對(duì) Save
函數(shù)的測(cè)試也將變得十分容易,比如下面示例代碼:
func TestSave(t *testing.T) { b := make([]byte, 0, 128) buf := bytes.NewBuffer(b) data := []byte("hello, golang") err := Save(buf, data) if err != nil { t.Errorf("want nil, actual %s", err.Error()) } saved := buf.Bytes() if !reflect.DeepEqual(saved, data) { t.Errorf("want %s, actual %s", string(data), string(saved)) } }
在這段代碼中,我們通過(guò) bytes.NewBuffer
創(chuàng)建了一個(gè) *bytes.Buffer
類型變量 buf
,由于 bytes.Buffer
實(shí)現(xiàn)了 Write
方法,進(jìn)而實(shí)現(xiàn)了 io.Writer
接口,我們可以合法地將變量 buf
傳遞給 Save
函數(shù)。之后我們可以從 buf
中取出 Save
函數(shù)寫(xiě)入的數(shù)據(jù)內(nèi)容與預(yù)期的數(shù)據(jù)做比對(duì),就可以達(dá)到對(duì) Save
函數(shù)進(jìn)行單元測(cè)試的目的了。在整個(gè)測(cè)試過(guò)程中,我們不需要?jiǎng)?chuàng)建任何磁盤(pán)文件或建立任何網(wǎng)絡(luò)連接。
看到這里,你應(yīng)該感受到了,用接口作為“關(guān)節(jié)(連接點(diǎn))”的好處很多!像上面圖中展示的那樣,接口可以將各個(gè)類型水平組合(連接)在一起。通過(guò)接口的編織,整個(gè)應(yīng)用程序不再是一個(gè)個(gè)孤立的“器官”,而是一幅完整的、有靈活性和擴(kuò)展性的靜態(tài)骨架結(jié)構(gòu)。
現(xiàn)在,我們已經(jīng)確定了接口承擔(dān)了應(yīng)用骨架的“關(guān)節(jié)”角色,接下來(lái)我們來(lái)看看接口是如何演好這一角色的。
三、接口應(yīng)用的幾種模式
前面已經(jīng)說(shuō)了,以接口為“關(guān)節(jié)”的水平組合方式,可以將各個(gè)垂直組合出的類型“耦合”在一起,從而編織出程序靜態(tài)骨架。而通過(guò)接口進(jìn)行水平組合的基本模式就是:使用接受接口類型參數(shù)的函數(shù)或方法。在這個(gè)基本模式基礎(chǔ)上,還有其他幾種“衍生品”。我們先從基本模式說(shuō)起,再往外延伸。
3.1 基本模式
接受接口類型參數(shù)的函數(shù)或方法是水平組合的基本語(yǔ)法,形式是這樣的:
func YourFuncName(param YourInterfaceType)
我們套用骨架關(guān)節(jié)的概念,用這幅圖來(lái)表示上面基本模式語(yǔ)法的運(yùn)用方法:
我們看到,函數(shù) / 方法參數(shù)中的接口類型作為“關(guān)節(jié)(連接點(diǎn))”,支持將位于多個(gè)包中的多個(gè)類型與 YourFuncName 函數(shù)連接到一起,共同實(shí)現(xiàn)某一新特性。
同時(shí),接口類型和它的實(shí)現(xiàn)者之間隱式的關(guān)系卻在不經(jīng)意間滿足了:依賴抽象(DIP)、里氏替換原則(LSP)、接口隔離(ISP)等代碼設(shè)計(jì)原則,這在其他語(yǔ)言中是需要很“刻意”地設(shè)計(jì)謀劃的,但對(duì) Go 接口來(lái)看,這一切卻是自然而然的。
這一水平組合的基本模式在 Go 標(biāo)準(zhǔn)庫(kù)、Go 社區(qū)第三方包中有著廣泛應(yīng)用,其他幾種模式也是從這個(gè)模式衍生的。下面我們看一下其他的各個(gè)衍生模式。
3.2 創(chuàng)建模式
Go 社區(qū)流傳一個(gè)經(jīng)驗(yàn)法則:“接受接口,返回結(jié)構(gòu)體(Accept interfaces, return structs
)”,這其實(shí)就是一種把接口作為“關(guān)節(jié)”的應(yīng)用模式。我這里把它叫做創(chuàng)建模式,是因?yàn)檫@個(gè)經(jīng)驗(yàn)法則多用于創(chuàng)建某一結(jié)構(gòu)體類型的實(shí)例。
下面是 Go 標(biāo)準(zhǔn)庫(kù)中,運(yùn)用創(chuàng)建模式創(chuàng)建結(jié)構(gòu)體實(shí)例的代碼摘錄:
// $GOROOT/src/sync/cond.go type Cond struct { ... ... L Locker } func NewCond(l Locker) *Cond { return &Cond{L: l} } // $GOROOT/src/log/log.go type Logger struct { mu sync.Mutex prefix string flag int out io.Writer buf []byte } func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{out: out, prefix: prefix, flag: flag} } // $GOROOT/src/log/log.go type Writer struct { err error buf []byte n int wr io.Writer } func NewWriterSize(w io.Writer, size int) *Writer { // Is it already a Writer? b, ok := w.(*Writer) if ok && len(b.buf) >= size { return b } if size <= 0 { size = defaultBufSize } return &Writer{ buf: make([]byte, size), wr: w, } }
我們看到,創(chuàng)建模式在 sync
、log
、bufio
包中都有應(yīng)用。以上面 log
包的 New
函數(shù)為例,這個(gè)函數(shù)用于實(shí)例化一個(gè) log.Logger
實(shí)例,它接受一個(gè) io.Writer
接口類型的參數(shù),返回 *log.Logger
。從 New
的實(shí)現(xiàn)上來(lái)看,傳入的 out
參數(shù)被作為初值賦值給了 log.Logger
結(jié)構(gòu)體字段 out
。
創(chuàng)建模式通過(guò)接口,在 NewXXX
函數(shù)所在包與接口的實(shí)現(xiàn)者所在包之間建立了一個(gè)連接。大多數(shù)包含接口類型字段的結(jié)構(gòu)體的實(shí)例化,都可以使用創(chuàng)建模式實(shí)現(xiàn)。這個(gè)模式比較容易理解,我們就不再深入了。
3.3 包裝器模式
在基本模式的基礎(chǔ)上,當(dāng)返回值的類型與參數(shù)類型相同時(shí),我們能得到下面形式的函數(shù)原型:
func YourWrapperFunc(param YourInterfaceType) YourInterfaceType
通過(guò)這個(gè)函數(shù),我們可以實(shí)現(xiàn)對(duì)輸入?yún)?shù)的類型的包裝,并在不改變被包裝類型(輸入?yún)?shù)類型)的定義的情況下,返回具備新功能特性的、實(shí)現(xiàn)相同接口類型的新類型。這種接口應(yīng)用模式我們叫它包裝器模式,也叫裝飾器模式。包裝器多用于對(duì)輸入數(shù)據(jù)的過(guò)濾、變換等操作。
下面就是 Go 標(biāo)準(zhǔn)庫(kù)中一個(gè)典型的包裝器模式的應(yīng)用:
// $GOROOT/src/io/io.go func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining } func (l *LimitedReader) Read(p []byte) (n int, err error) { // ... ... }
通過(guò)上面的代碼,我們可以看到,通過(guò) LimitReader
函數(shù)的包裝后,我們得到了一個(gè)具有新功能特性的 io.Reader
接口的實(shí)現(xiàn)類型,也就是 LimitedReader
。這個(gè)新類型在 Reader
的語(yǔ)義基礎(chǔ)上實(shí)現(xiàn)了對(duì)讀取字節(jié)個(gè)數(shù)的限制。
接下來(lái)我們?cè)倬唧w看 LimitReader
的一個(gè)使用示例:
func main() { r := strings.NewReader("hello, gopher!\n") lr := io.LimitReader(r, 4) if _, err := io.Copy(os.Stdout, lr); err != nil { log.Fatal(err) } }
運(yùn)行這個(gè)示例,我們得到了這個(gè)結(jié)果:
hell
我們看到,當(dāng)采用經(jīng)過(guò) LimitReader
包裝后返回的 io.Reader
去讀取內(nèi)容時(shí),讀到的是經(jīng)過(guò) LimitedReader
約束后的內(nèi)容,也就是只讀到了原字符串前面的 4 個(gè)字節(jié):“hell”。
由于包裝器模式下的包裝函數(shù)(如上面的 LimitReader
)的返回值類型與參數(shù)類型相同,因此我們可以將多個(gè)接受同一接口類型參數(shù)的包裝函數(shù)組合成一條鏈來(lái)調(diào)用,形式是這樣的:
YourWrapperFunc1(YourWrapperFunc2(YourWrapperFunc3(...)))
我們?cè)谏厦媸纠幕A(chǔ)上自定義一個(gè)包裝函數(shù):CapReader
,通過(guò)這個(gè)函數(shù)的包裝,我們能得到一個(gè)可以將輸入的數(shù)據(jù)轉(zhuǎn)換為大寫(xiě)的 Reader
接口實(shí)現(xiàn):
func CapReader(r io.Reader) io.Reader { return &capitalizedReader{r: r} } type capitalizedReader struct { r io.Reader } func (r *capitalizedReader) Read(p []byte) (int, error) { n, err := r.r.Read(p) if err != nil { return 0, err } q := bytes.ToUpper(p) for i, v := range q { p[i] = v } return n, err } func main() { r := strings.NewReader("hello, gopher!\n") r1 := CapReader(io.LimitReader(r, 4)) if _, err := io.Copy(os.Stdout, r1); err != nil { log.Fatal(err) } }
這里,我們將 CapReader
和 io.LimitReader
串在了一起形成一條調(diào)用鏈,這條調(diào)用鏈的功能變?yōu)椋航厝≥斎霐?shù)據(jù)的前四個(gè)字節(jié)并將其轉(zhuǎn)換為大寫(xiě)字母。這個(gè)示例的運(yùn)行結(jié)果與我們預(yù)期功能也是一致的:
HELL
3.4 適配器模式
適配器模式不是基本模式的直接衍生模式,但這種模式是后面中間件模式的前提,所以我們需要簡(jiǎn)單介紹下這個(gè)模式。
適配器模式的核心是適配器函數(shù)類型(Adapter Function Type)。適配器函數(shù)類型是一個(gè)輔助水平組合實(shí)現(xiàn)的“工具”類型。這里我要再?gòu)?qiáng)調(diào)一下,它是一個(gè)類型。它可以將一個(gè)滿足特定函數(shù)簽名的普通函數(shù),顯式轉(zhuǎn)換成自身類型的實(shí)例,轉(zhuǎn)換后的實(shí)例同時(shí)也是某個(gè)接口類型的實(shí)現(xiàn)者。
這里,我們來(lái)看一個(gè)應(yīng)用 http.HandlerFunc
的例子:
func greetings(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Welcome!") } func main() { http.ListenAndServe(":8080", http.HandlerFunc(greetings)) }
我們可以看到,這個(gè)例子通過(guò) http.HandlerFunc
這個(gè)適配器函數(shù)類型,將普通函數(shù) greetings
快速轉(zhuǎn)化為滿足 http.Handler
接口的類型。而 http.HandleFunc
這個(gè)適配器函數(shù)類型的定義是這樣的:
// $GOROOT/src/net/http/server.go type Handler interface { ServeHTTP(ResponseWriter, *Request) } type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
經(jīng)過(guò) HandlerFunc
的適配轉(zhuǎn)化后,我們就可以將它的實(shí)例用作實(shí)參,傳遞給接收 http.Handler
接口的 http.ListenAndServe
函數(shù),從而實(shí)現(xiàn)基于接口的組合。
3.5 中間件(Middleware)
最后,我們看下中間件這個(gè)應(yīng)用模式。中間件(Middleware)這個(gè)詞的含義可大可小。在 Go Web 編程中,“中間件”常常指的是一個(gè)實(shí)現(xiàn)了 http.Handler
接口的 http.HandlerFunc
類型實(shí)例。實(shí)質(zhì)上,這里的中間件就是包裝模式和適配器模式結(jié)合的產(chǎn)物。
我們來(lái)看一個(gè)例子:
func validateAuth(s string) error { if s != "123456" { return fmt.Errorf("%s", "bad auth token") } return nil } func greetings(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Welcome!") } func logHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t := time.Now() log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t) h.ServeHTTP(w, r) }) } func authHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := validateAuth(r.Header.Get("auth")) if err != nil { http.Error(w, "bad auth param", http.StatusUnauthorized) return } h.ServeHTTP(w, r) }) } func main() { http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings)))) }
我們看到,所謂中間件(如:logHandler
、authHandler
)本質(zhì)就是一個(gè)包裝函數(shù)(支持鏈?zhǔn)秸{(diào)用),但它的內(nèi)部利用了適配器函數(shù)類型(http.HandlerFunc
),將一個(gè)普通函數(shù)(比如例子中的幾個(gè)匿名函數(shù))轉(zhuǎn)型為實(shí)現(xiàn)了 http.Handler
的類型的實(shí)例。
運(yùn)行這個(gè)示例,并用 curl 工具命令對(duì)其進(jìn)行測(cè)試,我們可以得到下面結(jié)果:
$curl http://localhost:8080
bad auth param
$curl -H "auth:123456" localhost:8080/
Welcome!
從測(cè)試結(jié)果上看,中間件 authHandler
起到了對(duì) HTTP 請(qǐng)求進(jìn)行鑒權(quán)的作用。
四、接口使用的注意事項(xiàng)
盡量避免使用空接口作為函數(shù)參數(shù)類型
Go 語(yǔ)言之父 Rob Pike 曾說(shuō)過(guò):空接口不提供任何信息(The empty interface says nothing)。我們應(yīng)該怎么理解這句話的深層含義呢?
在 Go 語(yǔ)言中,一方面你不用像 Java 那樣顯式聲明某個(gè)類型實(shí)現(xiàn)了某個(gè)接口,但另一方面,你又必須聲明這個(gè)接口,這又與接口在 Java 等靜態(tài)類型語(yǔ)言中的工作方式更加一致。
這種不需要類型顯式聲明實(shí)現(xiàn)了某個(gè)接口的方式,可以讓種類繁多的類型與接口匹配,包括那些存量的、并非由你編寫(xiě)的代碼以及你無(wú)法編輯的代碼(比如:標(biāo)準(zhǔn)庫(kù))。Go 的這種處理方式兼顧安全性和靈活性,其中,這個(gè)安全性就是由 Go 編譯器來(lái)保證的,而為編譯器提供輸入信息的恰恰是接口類型的定義。
比如我們看下面的接口:
// $GOROOT/src/io/io.go type Reader interface { Read(p []byte) (n int, err error) }
Go 編譯器通過(guò)解析這個(gè)接口定義,得到接口的名字信息以及它的方法信息,在為這個(gè)接口類型參數(shù)賦值時(shí),編譯器就會(huì)根據(jù)這些信息對(duì)實(shí)參進(jìn)行檢查。這時(shí)你可以想一下,如果函數(shù)或方法的參數(shù)類型為空接口 interface{}
,會(huì)發(fā)生什么呢?
這恰好就應(yīng)了 Rob Pike 的那句話:“空接口不提供任何信息”。這里“提供”一詞的對(duì)象不是開(kāi)發(fā)者,而是編譯器。在函數(shù)或方法參數(shù)中使用空接口類型,就意味著你沒(méi)有為編譯器提供關(guān)于傳入實(shí)參數(shù)據(jù)的任何信息,所以,你就會(huì)失去靜態(tài)類型語(yǔ)言類型安全檢查的“保護(hù)屏障”,你需要自己檢查類似的錯(cuò)誤,并且直到運(yùn)行時(shí)才能發(fā)現(xiàn)此類錯(cuò)誤。
所以,建議 Gopher
盡可能地抽象出帶有一定行為契約的接口,并將它作為函數(shù)參數(shù)類型,盡量不要使用可以“逃過(guò)”編譯器類型安全檢查的空接口類型(interface{}
)。
在這方面,Go 標(biāo)準(zhǔn)庫(kù)已經(jīng)為我們作出了“表率”。全面搜索標(biāo)準(zhǔn)庫(kù)后,你可以發(fā)現(xiàn)以 interface{}
為參數(shù)類型的方法和函數(shù)少之甚少。不過(guò),也還有,使用 interface{}
作為參數(shù)類型的函數(shù)或方法主要有兩類:
- 容器算法類,比如:
container
下的heap
、list
和ring
包、sort
包、sync.Map
等; - 格式化 / 日志類,比如:
fmt
包、log
包等。
這些使用 interface{}
作為參數(shù)類型的函數(shù)或方法都有一個(gè)共同特點(diǎn),就是它們面對(duì)的都是未知類型的數(shù)據(jù),所以在這里使用具有“泛型”能力的 interface{}
類型。
五、小結(jié)
在使用接口前一定要搞清楚自己使用接口的原因,千萬(wàn)不能為了使用接口而使用接口。
接口與 Go 的“組合”的設(shè)計(jì)哲學(xué)息息相關(guān)。在 Go 語(yǔ)言中,組合是 Go 程序間各個(gè)部分的主要耦合方式。垂直組合可實(shí)現(xiàn)方法實(shí)現(xiàn)和接口定義的重用,更多用于在新類型的定義方面。而水平組合更多將接口作為“關(guān)節(jié)”,將各個(gè)垂直組合出的類型“耦合”在一起,從而編制出程序的靜態(tài)骨架。
通過(guò)接口進(jìn)行水平組合的基本模式,是“使用接受接口類型參數(shù)的函數(shù)或方法”,在這一基本模式的基礎(chǔ)上,我們還了解了幾個(gè)衍生模式:創(chuàng)建模式、包裝器模式與中間件模式。此外,我們還學(xué)習(xí)了一個(gè)輔助水平組合實(shí)現(xiàn)的“工具”類型:適配器函數(shù)類型,它也是實(shí)現(xiàn)中間件模式的前提。
最后需要我們牢記的是:我們要盡量避免使用空接口作為函數(shù)參數(shù)類型。一旦使用空接口作為函數(shù)參數(shù)類型,你將失去編譯器為你提供的類型安全保護(hù)屏障。
以上就是詳解Go語(yǔ)言中接口應(yīng)用模式或慣例介紹的詳細(xì)內(nèi)容,更多關(guān)于Go接口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang 中strings包的Replace的使用說(shuō)明
這篇文章主要介紹了golang 中strings包的Replace的使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03Go語(yǔ)言實(shí)現(xiàn)廣播式并發(fā)聊天服務(wù)器
本文主要介紹了Go語(yǔ)言實(shí)現(xiàn)廣播式并發(fā)聊天服務(wù)器,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08Go語(yǔ)言通過(guò)WaitGroup實(shí)現(xiàn)控制并發(fā)的示例詳解
Channel能夠很好的幫助我們控制并發(fā),但是在開(kāi)發(fā)習(xí)慣上與顯示的表達(dá)不太相同,所以在Go語(yǔ)言中可以利用sync包中的WaitGroup實(shí)現(xiàn)并發(fā)控制,本文就來(lái)和大家詳細(xì)聊聊WaitGroup如何實(shí)現(xiàn)控制并發(fā)2023-01-01Go如何實(shí)現(xiàn)HTTP請(qǐng)求限流示例
本篇文章主要介紹了Go如何實(shí)現(xiàn)HTTP請(qǐng)求限流示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04Go語(yǔ)言調(diào)用DeepSeek?API實(shí)現(xiàn)流式輸出和對(duì)話
DeepSeek是一個(gè)強(qiáng)大的AI模型服務(wù)平臺(tái),本文將詳細(xì)介紹如何使用Go語(yǔ)言調(diào)用DeepSeek?API實(shí)現(xiàn)流式輸出和對(duì)話功能,感興趣的小伙伴可以了解一下2025-02-02Go+Redis實(shí)現(xiàn)常見(jiàn)限流算法的示例代碼
限流是項(xiàng)目中經(jīng)常需要使用到的一種工具,一般用于限制用戶的請(qǐng)求的頻率,也可以避免瞬間流量過(guò)大導(dǎo)致系統(tǒng)崩潰,或者穩(wěn)定消息處理速率。這篇文章主要是使用Go+Redis實(shí)現(xiàn)常見(jiàn)的限流算法,需要的可以參考一下2023-04-04go內(nèi)存緩存BigCache之Entry封裝源碼閱讀
這篇文章主要介紹了go內(nèi)存緩存BigCache之Entry封裝源碼閱讀2023-09-09