在 Go 語言中,我為什么使用接口
強調(diào)一下是我個人的見解以及接口在 Go 語言中的意義。
如果您寫代碼已經(jīng)有了一段時間,我可能不需要過多解釋接口所帶來的好處,但是在深入探討 Go 語言中的接口前,我想花一兩分鐘先來簡單介紹一下接口。如果您對接口很熟悉,請先跳過下面這段。
接口的簡單介紹
在任一編程語言中,接口——方法或行為的集合,在功能和該功能的使用者之間構(gòu)建了一層薄薄的抽象層。在使用接口時,并不需要了解底層函數(shù)是如何實現(xiàn)的,因為接口隔離了各個部分(劃重點)。
跟不使用接口相比,使用接口的最大好處就是可以使代碼變得簡潔。例如,您可以創(chuàng)建多個組件,通過接口讓它們以統(tǒng)一的方式交互,盡管這些組件的底層實現(xiàn)差異很大。這樣就可以在編譯甚至運行的時候動態(tài)替換這些組件。
用 Go 的 io.Reader 接口舉個例子。io.Reader 接口的所有實現(xiàn)都有 Read(p []byte) (n int, err error) 函數(shù)。使用 io.Reader 接口的使用者不需要知道使用這個 Read 函數(shù)的時候那些字節(jié)從何而來。
具體到 Go 語言
在我使用 Go 語言的過程中,與我使用過的其他任何編程語言相比,我經(jīng)常發(fā)現(xiàn)其他的、不那么明顯的使用接口的原因。今天,我將介紹一個很普遍的,也是我遇到了很多次的使用接口的原因。
Go 語言沒有構(gòu)造函數(shù)
很多編程語言都有構(gòu)造函數(shù)。構(gòu)造函數(shù)是定義自定義類型(即 OO 語言中的類)時使用的一種建立對象的方法,它可以確保必須執(zhí)行的任何初始化邏輯均已執(zhí)行。
例如,假設(shè)所有 widgets 都必須有一個不變的,系統(tǒng)分配的標(biāo)識符。在 Java 中,這很容易實現(xiàn):
package?io.krancour.widget;
import?java.util.UUID;
public?class?Widget?{
????private?String?id;
????//?使用構(gòu)造函數(shù)初始化
????public?Widget()?{
????????id?=?UUID.randomUUID().toString();
????}
????public?String?getId()?{
????????return?id;
????}
}
class?App?{
????public?static?void?main(?String[]?args?){
????????Widget?w?=?new?Widget();
????????System.out.println(w.getId());
????}
}
從上面這個例子可以看到,沒有執(zhí)行初始化邏輯就無法實例化一個新的 Widget 。
但是 Go 語言沒有此功能。:(
在 Go 語言中,可以直接實例化一個自定義類型。
定義一個 Widget 類型:
package?widgets
type?Widget?struct?{
????id?string
}
func?(w?Widget)?ID()?string?{
????return?w.id
}
可以像這樣實例化和使用一個 widget:
package?main
import?(
????"fmt"
????"github.com/krancour/widgets"
)
func?main()?{
????w?:=?widgets.Widget{}
????fmt.Println(w.ID())
}
如果運行此示例,那么(也許)意料之中的結(jié)果是,打印出的 ID 是空字符串,因為它從未被初始化,而空字符串是字符串的“零值”。我們可以在 widgets 包中添加一個類似于構(gòu)造函數(shù)的函數(shù)來處理初始化:
package?widgets
import?uuid?"github.com/satori/go.uuid"
type?Widget?struct?{
????id?string
}
func?NewWidget()?Widget?{
????return?Widget{
????????id:?uuid.NewV4().String(),
????}
}
func?(w?Widget)?ID()?string?{
????return?w.id
}
然后我們簡單地修改 main 來使用這個類似于構(gòu)造函數(shù)的新函數(shù):
package?main
import?(
????"fmt"
????"github.com/krancour/widgets"
)
func?main()?{
????w?:=?widgets.NewWidget()
????fmt.Println(w.ID())
}
執(zhí)行該程序,我們得到了想要的結(jié)果。
但是仍然存在一個嚴(yán)重問題!我們的 widgets 包沒有強制用戶在初始一個 widget 的時候使用我們的構(gòu)造函數(shù)。
變量私有化
首先我們嘗試把自定義類型的變量私有化,以此來強制用戶使用我們規(guī)定的構(gòu)造函數(shù)來初始化 widget。在 Go 語言中,類型名、函數(shù)名的首字母是否大寫決定它們是否可被其他包訪問。名稱首字母大寫的可被訪問(也就是 public ),而名稱首字母小寫的不可被訪問(也就是 private )。所以我們把類型 Widget 改為類型 widget :
package?widgets
import?uuid?"github.com/satori/go.uuid"
type?widget?struct?{
????id?string
}
func?NewWidget()?widget?{
????return?widget{
????????id:?uuid.NewV4().String(),
????}
}
func?(w?widget)?ID()?string?{
????return?w.id
}
我們的 main 代碼保持不變,這次我們得到了一個 ID 。這比我們想要的要近了一步,但是我們在此過程中犯了一個不太明顯的錯誤。類似于構(gòu)造函數(shù)的 NewWidget 函數(shù)返回了一個私有的實例。盡管編譯器對此不會報錯,但這是一種不好的做法,下面是原因解釋。
在 Go 語言中,**包是復(fù)用的基本單位。其他語言中的類**是復(fù)用的基本單位。如前所述,任何無法被外部訪問的內(nèi)容實質(zhì)上都是“包私有”,是該包的內(nèi)部實現(xiàn)細節(jié),對于使用這個包的使用者來說不重要。因此,Go 的文檔生成工具 godoc 不會為私有的函數(shù)、類型等生成文檔。
當(dāng)一個公開的構(gòu)造函數(shù)返回一個私有的 widget 實例,實際上就陷入了一條死胡同。調(diào)用這個函數(shù)的人哪怕有這個實例,也絕對在文檔里找不到任何關(guān)于這個實例類型的描述,也更不知道 ID() 這個函數(shù)。Go 社區(qū)非常重視文檔,所以這樣做是不會被接受的。
輪到接口上場了
回顧一下,到目前為止,我們寫了一個類似于構(gòu)造函數(shù)的函數(shù)來解決 Go 語言缺乏構(gòu)造函數(shù)的問題,但是為了確保人們用該函數(shù)而不是直接實例化 Widget ,我們更改了該類型的可見性——將其重命名為 widget,即私有化了。雖然編譯器不會報錯,但是文檔中不會出現(xiàn)對這個私有類型的描述。不過,我們距離想要的目標(biāo)還近了一步。接下來就要使用接口來完成后續(xù)的了。
通過創(chuàng)建一個**可被訪問的**、widget 類型可以實現(xiàn)的接口,我們的構(gòu)造函數(shù)可以返回一個公開的類型實例,并且會顯示在 godoc 文檔中。同時,這個接口的底層實現(xiàn)依然是私有的,使用者無法直接創(chuàng)建一個實例。
package?widgets
import?uuid?"github.com/satori/go.uuid"
//?Widget?is?a?...
type?Widget?interface?{
????//?ID?返回這個?widget?的唯一標(biāo)識符
????ID()?string
}
type?widget?struct?{
????id?string
}
//?NewWidget()?返回一個新的?Widget?實例
func?NewWidget()?Widget?{
????return?widget{
????????id:?uuid.NewV4().String(),
????}
}
func?(w?widget)?ID()?string?{
????return?w.id
}
總結(jié)
我希望我已經(jīng)充分地闡述了 Go 語言的這一特質(zhì)——構(gòu)造函數(shù)的缺失反而促進了接口的使用。
在我的下一篇文章中,我將介紹一種幾乎與之相反的場景——在其他語言中要使用接口但是在 Go 語言中卻不必。
via: https://medium.com/@kent.rancourt/go-pointers-why-i-use-interfaces-in-go-338ae0bdc9e4
作者:Kent Rancourt[1]譯者:zhiyu-tracy-yang[2]校對:polaris1119[3]
本文由 GCTT[4] 原創(chuàng)編譯,Go 中文網(wǎng)[5] 榮譽推出
參考資料
Kent Rancourt: https://medium.com/@kent.rancourt
[2]zhiyu-tracy-yang: https://github.com/zhiyu-tracy-yang
[3]polaris1119: https://github.com/polaris1119
[4]GCTT: https://github.com/studygolang/GCTT
[5]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
