轉載
原文地址:https://studygolang.com/articles/27152?fr=sidebar
接口的簡單介紹
在任一編程語言中,接口——方法或行為的集合,在功能和該功能的使用者之間構建了一層薄薄的抽象層。在使用接口時,並不需要了解底層函數是如何實現的,因為接口隔離了各個部分(划重點)。
跟不使用接口相比,使用接口的最大好處就是可以使代碼變得簡潔。例如,您可以創建多個組件,通過接口讓它們以統一的方式交互,盡管這些組件的底層實現差異很大。這樣就可以在編譯甚至運行的時候動態替換這些組件。
用 Go 的 io.Reader
接口舉個例子。io.Reader
接口的所有實現都有 Read(p []byte) (n int, err error)
函數。使用 io.Reader
接口的使用者不需要知道使用這個 Read
函數的時候那些字節從何而來。
具體到 Go 語言
在我使用 Go 語言的過程中,與我使用過的其他任何編程語言相比,我經常發現其他的、不那么明顯的使用接口的原因。今天,我將介紹一個很普遍的,也是我遇到了很多次的使用接口的原因。
Go 語言沒有構造函數
很多編程語言都有構造函數。構造函數是定義自定義類型(即 OO 語言中的類)時使用的一種建立對象的方法,它可以確保必須執行的任何初始化邏輯均已執行。
例如,假設所有 widgets
都必須有一個不變的,系統分配的標識符。在 Java 中,這很容易實現:
package io.krancour.widget; import java.util.UUID; public class Widget { private String id; // 使用構造函數初始化 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()); } }
從上面這個例子可以看到,沒有執行初始化邏輯就無法實例化一個新的 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()) }
如果運行此示例,那么(也許)意料之中的結果是,打印出的 ID 是空字符串,因為它從未被初始化,而空字符串是字符串的“零值”。 我們可以在 widgets
包中添加一個類似於構造函數的函數來處理初始化:
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
來使用這個類似於構造函數的新函數:
package main import ( "fmt" "github.com/krancour/widgets" ) func main() { w := widgets.NewWidget() fmt.Println(w.ID()) }
執行該程序,我們得到了想要的結果。
但是仍然存在一個嚴重問題!我們的 widgets
包沒有強制用戶在初始一個 widget
的時候使用我們的構造函數。
變量私有化
首先我們嘗試把自定義類型的變量私有化,以此來強制用戶使用我們規定的構造函數來初始化 widget
。在 Go 語言中,類型名、函數名的首字母是否大寫決定它們是否可被其他包訪問。名稱首字母大寫的可被訪問(也就是 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 。這比我們想要的要近了一步,但是我們在此過程中犯了一個不太明顯的錯誤。類似於構造函數的 NewWidget
函數返回了一個私有的實例。盡管編譯器對此不會報錯,但這是一種不好的做法,下面是原因解釋。
在 Go 語言中,包是復用的基本單位。其他語言中的類是復用的基本單位。如前所述,任何無法被外部訪問的內容實質上都是“包私有”,是該包的內部實現細節,對於使用這個包的使用者來說不重要。因此,Go 的文檔生成工具 godoc
不會為私有的函數、類型等生成文檔。
當一個公開的構造函數返回一個私有的 widget
實例,實際上就陷入了一條死胡同。調用這個函數的人哪怕有這個實例,也絕對在文檔里找不到任何關於這個實例類型的描述,也更不知道 ID()
這個函數。Go 社區非常重視文檔,所以這樣做是不會被接受的。
輪到接口上場了
回顧一下,到目前為止,我們寫了一個類似於構造函數的函數來解決 Go 語言缺乏構造函數的問題,但是為了確保人們用該函數而不是直接實例化 Widget
,我們更改了該類型的可見性——將其重命名為 widget
,即私有化了。雖然編譯器不會報錯,但是文檔中不會出現對這個私有類型的描述。不過,我們距離想要的目標還近了一步。接下來就要使用接口來完成后續的了。
通過創建一個可被訪問的、widget
類型可以實現的接口,我們的構造函數可以返回一個公開的類型實例,並且會顯示在 godoc
文檔中。同時,這個接口的底層實現依然是私有的,使用者無法直接創建一個實例。
package widgets import uuid "github.com/satori/go.uuid" // Widget is a ... type Widget interface { // ID 返回這個 widget 的唯一標識符 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 }
總結
Go 語言的這一特質——構造函數的缺失反而促進了接口的使用。