go語言中的介面
原文: https://medium.com/rungo/interfaces-in-go-ab1601159b3a
翻譯:devabel
介面是golang中實現多型性的唯一好途徑。
什麼是介面?
我們在結構和方法課程中討論了很多關於物件和行為的內容。 我們學習了結構體(以及其他非結構型別)實現方法。 介面是一組方法簽名的集合,然後我們可以定義一個結構體實現該介面所有方法。因此,介面就是定義了物件的行為。
例如,結構體Dog可以walk和bark, 如果一個介面聲明瞭walk和bark的方法簽名,而Dog實現了walk和bark方法,那麼Dog就實現了該介面。
介面的主要工作是僅提供由方法名稱,輸入引數和返回型別組成的方法簽名集合。 由型別(例如struct結構體)來宣告方法並實現它們。
如果您曾經是面向物件的程式設計師,您肯定會經常使用implements關鍵字來實現介面。 但是在go中,你沒有明確提到一個型別是否實現了一個介面。 如果一個型別實現了在介面中定義的簽名方法,則稱該型別實現該介面。 就像說它像鴨子一樣走路,像鴨子一樣游泳,像鴨子一樣嘎嘎叫,那就是鴨子。
定義介面
與struct類似,我們需要使用類型別名,通過interface關鍵字來簡化介面宣告。
type Shape interface { Area() float64 Perimeter() float64 }
上面的程式碼中,我們定義了Shape介面,它有兩個方法Area和Perimeter,他們不接收任何引數並返回float64。 任何實現這兩個方法的型別我們都認為它實現了Shape介面。
由於interface也是一種型別,我們可以建立它的型別的變數。 在上面的例子中,我們可以建立一個型別為介面Shape的變數s。

1-yCE2tzPRap4Uc1Vn4iKHKQ.png
在我們對上面例子輸出結果困惑前,讓我解釋一下。 介面有兩種型別。 靜態型別的介面是介面本身,例如上面的程式中的Shape。 介面沒有靜態值,而是指向動態值。 介面型別的變數可以儲存實現介面的Type的值。 該型別的值成為介面的動態值,該型別成為介面的動態型別。
從上面的結果,我們可以看到介面的值是nil而且介面的型別也是nil。 這是因為此時,介面不知道是誰會實現它。 當我們使用帶有介面引數的fmt包中的Println函式時,它指向介面的動態值,而Printf函式中的%T語法指的是介面的動態型別。 但實際上,介面的型別是Shape。
實現介面
讓我們定義Shape介面提供的簽名方法Area和Perimeter方法。 同時我們建立一個Shape結構體並使其實現Shape介面。

1-qqju8nevflb3D5-gDmpnBw.png
所以在上面的程式中,我們建立了Shape介面和矩形結構體型別Rect。 然後我們使用Rect接收器型別定義了Area和Perimeter等方法。 因此Rect實現了這些方法。 由於這些方法是由Shape介面定義的,因此Rect實現了Shape介面。
我們可以通過建立nil介面併為其指定Rect型別的結構來確認。 由於Rect實現了Shape介面,因此完全有效。 從上面的結果可以看出,s的動態型別現在是Rect,s的動態值是struct Rect的值,即{5 4}。 動態因為,我們可以為不同型別分配新的結構,也實現介面Shape。
有時,動態型別的介面也稱為具體型別,因為當我們訪問介面型別時,它返回它的基礎動態值的型別,並且它的靜態型別保持隱藏。
我們可以在s上呼叫Area方法,因為s的具體型別是Rect而Rect實現了Area方法。 我們還可以看到,我們可以將s與型別為Rect的r struct進行比較,因為它們都具有相同的Rect具體型別且具有相同的值。
讓我們改變s的動態型別和值。

1-k8gbNmpSgw1zVwfnmig6uA.png
如果你閱讀結構和方法課程,那麼上面的程式不應該讓你感到驚訝。 由於新的struct型別Circle也實現了Shape介面,我們可以為s分配一個Circle型別的結構。
現在我想你可以理解為什麼介面的型別和價值是動態的。 就像我們在切片課中看到的那樣切片保持對陣列的引用,我們可以說通過動態保持對基礎型別的引用,介面也以類似的方式工作。
你能猜出下面的程式會發生什麼嗎?

1-icnz60F1isROaYU-dqw7AA.png
在上面的程式中,我們刪除了Perimeter方法。 上面的程式不會編譯通過,編譯器會丟擲錯誤
program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment: Rect does not implement Shape (missing Perimeter method)
從上面的錯誤中可以明顯看出,為了成功實現介面,您需要實現介面宣告的所有方法。
空介面
當介面沒有方法時,它被稱為空介面。 這由interface{}表示。 由於空介面沒有任何方法,因此所有型別都實現了該介面。
您是否想知道fmt包中的Println函式如何接受在控制檯上列印的不同型別的資料。 由於空介面,這是可能的。 讓我們看看它是如何工作的。
讓我們建立一個函式說明,它接受一個空介面並解釋介面的動態值和型別。

1-FKuEaecQMtslenQ-VdJW1A.png
在上面的程式中,我們建立了一個自定義字串型別MyString和一個struct型別Rect。 由於explain函式接受空介面,我們可以將MyString和Rect型別的變數傳遞給它,因為型別為空介面的引數i可以儲存任何型別的值,因為所有型別都實現它。
多介面
一個型別可以實現多個介面。 我們來看一個例子吧。

1-sUynQOybf45TpWs7LDoNTg.png
在上面的程式中,我們使用Area方法建立了Shape介面,使用Volume方法建立了Object介面。 由於struct type Cube實現了這兩種方法,因此它實現了這兩種介面。 因此,我們可以將struct type Cube的值賦給Shape或Object型別的變數。
我們期望s具有c和o的動態值也具有c的動態值。 我們在Shape介面的s上使用了Area方法,因為它定義了Object介面型別的Area方法和Volume方法,因為它定義了Volume方法。 但是如果我們在o上使用Volume方法和在o上使用Area方法會發生什麼。
讓我們對上面的程式進行更改,看看會發生什麼
fmt.Println("area of s of interface type Shape is", s.Volume()) fmt.Println("volume of o of interface type Object is", o.Area())
以上變化產生以下結果
program.go:31: s.Volume undefined (type Shape has no field or method Volume) program.go:32: o.Area undefined (type Object has no field or method Area)
程式將無法編譯,因為靜態型別的s是Shape而o是Object。 為了使其工作,我們需要以某種方式提取這些介面的基礎值。 這可以使用型別斷言來完成。
型別斷言
我們可以使用語法i.(Type)找出介面的基礎動態值,其中i是介面,Type是實現介面i的型別。 Go將檢查i的動態型別是否與Type相同。
因此,讓我們重寫前面的例子並提取介面的動態值。

1-Jsysj6iM4A-37XuhEvP98A.png
從上面的程式,我們現在可以訪問變數c中介面s的底層值,它是Cube型別的結構。 現在,我們可以在c上使用Area和Volume方法。
在型別斷言語法i.(Type)中,如果Type沒有實現介面(型別)i那麼go編譯器會丟擲錯誤。 但是如果Type實現了介面,但是我沒有Type的具體值,那麼go將在執行時出現混亂。 幸運的是,還有另一種型別斷言語法的變體,即
value, ok := i.(Type)
在上面的語法中,我們可以檢查使用ok變數,如果Type實現介面(型別)i,我有具體型別Type。 如果是,那麼ok將為true,否則為false,value為struct的零值。
我還有一個問題。 我們如何知道介面的底層值是否實現了任何其他介面? 型別斷言也可以這樣做。 如果Type in斷言語法是interface,那麼go將檢查i的動態型別是否實現介面Type。

1-K0TVqBr1WSnB1uuzCpbHyg.png
由於Cube結構不實現Skin介面,我們將ok2視為false,將value2視為nil。 如果我們使用更簡單的v := i.(type)語法,那麼我們的程式會報錯
panic: interface conversion: main.Cube is not main.Skin: missing method Color
型別開關
我們已經看到空介面和它的使用。 讓我們想一下使用解釋函式的例子,如前所述。 由於explain函式的引數型別是空介面,我們可以將任何引數傳遞給它。 但是如果傳遞的引數是一個字串,我們希望explain函式以大寫形式列印它。 我們可以從字串包中使用ToUpper函式,但由於它只接受字串引數,我們需要確保內部解釋函式中具體型別的空介面i是字串。
這可以使用Type開關完成。 型別切換的語法類似於型別斷言,它是i。(type)其中i是介面,type是固定關鍵字。 使用這個我們可以獲得介面的具體型別而不是值。 但是這種語法只適用於switch語句。
我們來看一個例子吧

1-auPE-81-CFtMBuJktYnLog.png
在上面的程式中我們修改瞭解釋函式以使用型別切換。 當使用任何型別呼叫explain函式時,我會收到其值並鍵入動態型別。 在switch中使用i.(type)語句,我們可以訪問該動態型別。 使用switch中的case,我們可以做條件操作。 在字串大小寫的情況下,我們使用strings.ToUpper函式將字串轉換為大寫。 但由於它只接受字串型別,我們需要i的基礎值,它是字串型別,因此我們使用了型別斷言。
嵌入式介面
在go中,介面不能實現其他介面或擴充套件它們,但我們可以通過合併兩個或多個介面來建立新介面。 讓我們重寫我們的Shape-Cube程式。

1-UU13NxiyaYqNCE_t22YIxg.png
在上面的程式中,由於Cube實現了方法Area和Volume,它實現了Shape和Object介面。 但由於介面Material是這些介面的嵌入式介面,Cube也必須實現它。 發生這種情況是因為像匿名巢狀結構一樣,巢狀介面的所有方法都被提升為父介面。
指標與值接收器
到目前為止,我們已經看到了帶有值接收器 對於接受指標接收器的方法,介面是否正常。 我們來看看吧。

1-fPMsy7XAqXVPA2oZOl-1wQ.png
上面的程式不會編譯而且會丟擲編譯錯誤
program.go:27: cannot use Rect literal (type Rect) as type Shape in assignment: Rect does not implement Shape (Area method has pointer receiver)
為什麼會這樣? 我們可以清楚地看到struct型別Rect正在實現介面Shape所宣告的所有方法,那麼為什麼我們得到Rect不會實現Shape錯誤。 如果你仔細閱讀錯誤,它說區域方法有指標接收器。 那麼如果Area方法有指標接收器呢?
好吧,我們已經看到了結構課程,指標接收器的方法將對指標或值都起作用,如果我們在上面的程式中使用r.Area(),它就會編譯得很好。
但是在介面的情況下,如果方法有指標接收器,那麼介面將具有動態型別的指標而不是動態型別的值。 因此,當我們為介面變數分配型別值時,我們需要分配型別為value的指標。 讓我們用這個概念重寫上面的程式。

1-lj6zb6raHdkZn3tWBI2LGA.png
我們所做的唯一改變就是25行,用r的值代替,我們使用指向r的指標。 因此,s的具體值現在是一個指標。 以上程式將編譯正常。
使用介面
我們已經學習了介面,我們看到它們可以採用不同的形式。 這就是多型性的定義。 介面在需要傳遞給它們的許多型別的引數的函式和方法的情況下非常有用,例如接受所有型別的值的Println函式。 如果你看到Println函式的語法,就像
func Println(a ...interface{}) (n int, err error)
這也是一種可變函式。
當多個型別實現相同的介面時,使用相同的程式碼可以很容易地使用它們。 因此,只要我們可以使用介面,我們就應該儘量使用它。