Go 面向?qū)ο缶幊唐ㄎ澹航涌诙x及實(shí)現(xiàn)

接口在 Go 語(yǔ)言中有著至關(guān)重要的地位,如果說(shuō) goroutine 和 channel 是支撐起 Go 語(yǔ)言并發(fā)模型的基石,那么接口就是 Go 語(yǔ)言整個(gè)類型系統(tǒng)的基石。Go 語(yǔ)言的接口不單單只是接口,下面就讓我們一步步來(lái)探索 Go 語(yǔ)言的接口特性。
一、傳統(tǒng)侵入式接口實(shí)現(xiàn)
和類的實(shí)現(xiàn)相似,Go 語(yǔ)言的接口和其他語(yǔ)言中提供的接口概念完全不同。以 Java、PHP 為例,接口主要作為不同類之間的契約(Contract)存在,對(duì)契約的實(shí)現(xiàn)是強(qiáng)制的,體現(xiàn)在具體的細(xì)節(jié)上就是如果一個(gè)類實(shí)現(xiàn)了某個(gè)接口,就必須實(shí)現(xiàn)該接口聲明的所有方法,這個(gè)叫「履行契約」:
// 聲明一個(gè)'iTemplate'接口
interface iTemplate
{
public function setVariable($name, $var);
public function getHtml($template);
}
// 實(shí)現(xiàn)接口
// 下面的寫法是正確的
class Template implements iTemplate
{
private $vars = array();
public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}
public function getHtml($template)
{
foreach($this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}
return $template;
}
}
這個(gè)時(shí)候,如果有另外有一個(gè)接口 iTemplate2 聲明了與 iTemplate 完全一樣的接口方法,甚至名字也叫 iTemplate,只不過(guò)位于不同的命名空間下,編譯器也會(huì)認(rèn)為上面的類 Template 只實(shí)現(xiàn)了 iTemplate 而沒(méi)有實(shí)現(xiàn) iTemplate2 接口。
這在我們之前的認(rèn)知中是理所當(dāng)然的,無(wú)論是類與類之間的繼承,還是類與接口之間的實(shí)現(xiàn),在 Java、PHP 這種單繼承語(yǔ)言中,存在著嚴(yán)格的層級(jí)關(guān)系,一個(gè)類只能直接繼承自一個(gè)父類,一個(gè)類也只能實(shí)現(xiàn)指定的接口,如果沒(méi)有顯式聲明繼承自某個(gè)父類或者實(shí)現(xiàn)某個(gè)接口,那么這個(gè)類就與該父類或者該接口沒(méi)有任何關(guān)系。
我們把這種接口稱為侵入式接口,所謂「侵入式」指的是實(shí)現(xiàn)類必須明確聲明自己實(shí)現(xiàn)了某個(gè)接口。這種實(shí)現(xiàn)方式雖然足夠明確和簡(jiǎn)單明了,但也存在一些問(wèn)題,尤其是在設(shè)計(jì)標(biāo)準(zhǔn)庫(kù)的時(shí)候,因?yàn)闃?biāo)準(zhǔn)庫(kù)必然涉及到接口設(shè)計(jì),接口的需求方是業(yè)務(wù)實(shí)現(xiàn)類,只有具體編寫業(yè)務(wù)實(shí)現(xiàn)類的時(shí)候才知道需要定義哪些方法,而在此之前,標(biāo)準(zhǔn)庫(kù)的接口就已經(jīng)設(shè)計(jì)好了,我們要么按照約定好的接口進(jìn)行實(shí)現(xiàn),如果沒(méi)有合適的接口需要自己去設(shè)計(jì),這里的問(wèn)題就是接口的設(shè)計(jì)和業(yè)務(wù)的實(shí)現(xiàn)是分離的,接口的設(shè)計(jì)者并不能總是預(yù)判到業(yè)務(wù)方要實(shí)現(xiàn)哪些功能,這就造成了設(shè)計(jì)與實(shí)現(xiàn)的脫節(jié)。
接口的過(guò)分設(shè)計(jì)會(huì)導(dǎo)致某些聲明的方法實(shí)現(xiàn)類完全不需要,如果設(shè)計(jì)的太簡(jiǎn)單又會(huì)導(dǎo)致無(wú)法滿足業(yè)務(wù)的需求,這確實(shí)是一個(gè)問(wèn)題,而且脫離了用戶使用場(chǎng)景討論這些并沒(méi)有意義,以 PHP 自帶的 SessionHandlerInterface 接口為例,該接口聲明的接口方法如下:
SessionHandlerInterface {
/* 方法 */
abstract public close ( void ) : bool
abstract public destroy ( string $session_id ) : bool
abstract public gc ( int $maxlifetime ) : int
abstract public open ( string $save_path , string $session_name ) : bool
abstract public read ( string $session_id ) : string
abstract public write ( string $session_id , string $session_data ) : bool
}
用戶自定義的 Session 管理器需要實(shí)現(xiàn)該接口,也就是要實(shí)現(xiàn)該接口聲明的所有方法,但是實(shí)際在做業(yè)務(wù)開(kāi)發(fā)的時(shí)候,某些方法其實(shí)并不需要實(shí)現(xiàn),比如如果我們基于 Redis 或 Memcached 作為 Session 存儲(chǔ)器的話,它們自身就包含了過(guò)期回收機(jī)制,所以 gc 方法根本不需要實(shí)現(xiàn),又比如 close 方法對(duì)于大部分驅(qū)動(dòng)來(lái)說(shuō),也是沒(méi)有什么意義的。
正是因?yàn)檫@種不合理的設(shè)計(jì),所以在編寫 PHP 類庫(kù)中的每個(gè)接口時(shí)都需要糾結(jié)以下兩個(gè)問(wèn)題(Java 也類似):
一個(gè)接口需要聲明哪些接口方法?
如果多個(gè)類實(shí)現(xiàn)了相同的接口方法,應(yīng)該如何設(shè)計(jì)接口?比如上面這個(gè)
SessionHandlerInterface,有沒(méi)有必要拆分成多個(gè)更細(xì)分的接口,以適應(yīng)不同實(shí)現(xiàn)類的需要?
接下我們來(lái)看看 Go 語(yǔ)言的接口是如何避免這些問(wèn)題的。
二、Go 語(yǔ)言的接口實(shí)現(xiàn)
在 Go 語(yǔ)言中,類對(duì)接口的實(shí)現(xiàn)和子類對(duì)父類的繼承一樣,并沒(méi)有提供類似 implement 這種關(guān)鍵字顯式聲明該類實(shí)現(xiàn)了哪個(gè)接口,一個(gè)類只要實(shí)現(xiàn)了某個(gè)接口要求的所有方法,我們就說(shuō)這個(gè)類實(shí)現(xiàn)了該接口。
例如,我們定義了一個(gè) File 類,并實(shí)現(xiàn)了 Read()、Write()、Seek()、Close() 四個(gè)方法:
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error
假設(shè)我們有如下接口(Go 語(yǔ)言通過(guò)關(guān)鍵字 interface 來(lái)聲明接口,以示和結(jié)構(gòu)體類型的區(qū)別,花括號(hào)內(nèi)包含的是待實(shí)現(xiàn)的方法集合):
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
盡管 File 類并沒(méi)有顯式實(shí)現(xiàn)這些接口,甚至根本不知道這些接口的存在,但是我們說(shuō) File 類實(shí)現(xiàn)了這些接口,因?yàn)?File 類實(shí)現(xiàn)了上述所有接口聲明的方法。當(dāng)一個(gè)類的成員方法集合包含了某個(gè)接口聲明的所有方法,換句話說(shuō),如果一個(gè)接口的方法集合是某個(gè)類成員方法集合的子集,我們就認(rèn)為該類實(shí)現(xiàn)了這個(gè)接口。
與 Java、PHP 相對(duì),我們把 Go 語(yǔ)言的這種接口稱作非侵入式接口,因?yàn)轭惻c接口的實(shí)現(xiàn)關(guān)系不是通過(guò)顯式聲明,而是系統(tǒng)根據(jù)兩者的方法集合進(jìn)行判斷。這樣做有兩個(gè)好處:
其一,Go 語(yǔ)言的標(biāo)準(zhǔn)庫(kù)不需要繪制類庫(kù)的繼承/實(shí)現(xiàn)樹(shù)圖,在 Go 語(yǔ)言中,類的繼承樹(shù)并無(wú)意義,你只需要知道這個(gè)類實(shí)現(xiàn)了哪些方法,每個(gè)方法是干什么的就足夠了。
其二,定義接口的時(shí)候,只需要關(guān)心自己應(yīng)該提供哪些方法即可,不用再糾結(jié)接口需要拆得多細(xì)才合理,也不需要為了實(shí)現(xiàn)某個(gè)接口而引入接口所在的包,接口由使用方按需定義,不用事先設(shè)計(jì),也不用考慮之前是否有其他模塊定義過(guò)類似接口。
這樣一來(lái),就完美地避免了傳統(tǒng)面向?qū)ο缶幊讨械慕涌谠O(shè)計(jì)問(wèn)題。
三、通過(guò)組合實(shí)現(xiàn)接口繼承
我們知道在 Java、PHP 等傳統(tǒng)面向?qū)ο缶幊陶Z(yǔ)言中,支持通過(guò) extends 關(guān)鍵字實(shí)現(xiàn)接口之間的繼承關(guān)系:
interface A
{
public function foo();
}
interface B extends A
{
public function bar();
}
在上述代碼中,我們定義了兩個(gè) PHP 接口:A 和 B,其中接口 B 繼承自 A,這樣一來(lái),如果某個(gè)類實(shí)現(xiàn)了接口 B,則必須實(shí)現(xiàn)這兩個(gè)接口中聲明的方法,否則會(huì)報(bào)錯(cuò)。
Go 語(yǔ)言也支持類似的「接口繼承」特性,但是由于不支持 extends 關(guān)鍵字,所以其實(shí)現(xiàn)和類的繼承一樣,是通過(guò)組合來(lái)完成的。以上面這個(gè) PHP 示例為例,在 Go 語(yǔ)言中,我們可以這樣通過(guò)接口組合來(lái)實(shí)現(xiàn)接口繼承,就像類的組合一樣:
type A interface {
Foo()
}
type B interface {
A
Bar()
}
然后我們定義一個(gè)類 T 實(shí)現(xiàn)接口 B:
type T struct {}
func (t T) Foo() {
fmt.Println("call Foo function from interface A.")
}
func (t T) Bar() {
fmt.Println("call Bar function from interface B.")
}
不過(guò),在 Go 語(yǔ)言中,又與傳統(tǒng)的接口繼承有些不同,因?yàn)榻涌趯?shí)現(xiàn)不是強(qiáng)制的,是根據(jù)類實(shí)現(xiàn)的方法來(lái)動(dòng)態(tài)判定的,比如我們上面的 T 類可以只實(shí)現(xiàn) Foo 方法,也可以只實(shí)現(xiàn) Bar 方法,也可以都不實(shí)現(xiàn)。如果只實(shí)現(xiàn)了 Foo 方法,則 T 實(shí)現(xiàn)了接口 A;如果只實(shí)現(xiàn)了 Bar 方法,則既沒(méi)有實(shí)現(xiàn)接口 A 也沒(méi)有實(shí)現(xiàn)接口 B,只有兩個(gè)方法都實(shí)現(xiàn)了系統(tǒng)才會(huì)判定實(shí)現(xiàn)了接口 B。
可以認(rèn)為接口組合是匿名類型組合(沒(méi)有顯式為組合類型設(shè)置對(duì)應(yīng)的屬性名稱)的一個(gè)特定場(chǎng)景,只不過(guò)接口只包含方法,而不包含任何屬性。Go 語(yǔ)言底層很多包就是基于接口組合實(shí)現(xiàn)的,比如 io 里面的 Reader、Writer、ReadWriter 這些接口:
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
(本文完)
學(xué)習(xí)過(guò)程中有任何問(wèn)題,可以通過(guò)下面的評(píng)論功能或加入「Go 語(yǔ)言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點(diǎn)擊頁(yè)面左下角閱讀原文鏈接查看最新更新的教程。
