go 學習筆記之無心插柳柳成蔭的介面和無為而治的空介面

snowdreams1006發表於2019-09-10

如果你還了解程式設計概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點選 go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計? ,否則的話,請自動忽略上文,繼續探索 Go 語言的介面有什麼不同之處.

如無法自動跳轉到公眾號「雪之夢技術驛站」文章,可以點選我的頭像,動動你的小手翻翻歷史文章,相信聰明的你一定可以找到相關文章.

介面是物件導向程式設計風格中繼封裝概念後的另一個重要概念,封裝包含兩方面含義:資料和行為的封裝.

關於封裝的概念這裡同樣不再贅述,有興趣的話,可以閱讀go 學習筆記之詳細說一說封裝是怎麼回事.

當現實世界中的事物或者實際需求轉移到程式設計世界中去實現時,這時候就需要進行建模,建立合適的模型來反映現實的事物,為了模型的緊湊性以及更好的複用性.程式設計世界的前輩們總結出封裝的概念,並在此基礎上進一步衍生出一系列的程式設計風格,其中就包括物件導向中的繼承概念.

關於繼承的概念這裡同樣不再贅述,有興趣的話,可以閱讀go 學習筆記之是否支援以及如何實現繼承.

封裝和繼承都是在描述同類事物模型彼此共性,正如貓和狗都是動物,運用繼承的概念表示的話,貓和狗繼承自動物.貓和狗不僅具備各自特殊的屬性和行為,還具備一般動物的屬性和行為.

然而,並不是只有同類事物才具有相同特徵.家禽鴨子是鴨子,玩具太空鴨也是鴨子,看似是同類事物實際卻只有某一方面的行為相同而已,一個有生命,另一個無生命.

針對這種情況下統一共性行為的方法也就是介面,是對同類事物或者不同類事物的某一方面行為的統一抽象,滿足該行為規範的封裝物件稱之為實現了該介面.

介面描述的是規範約束和實現的一種規則,介面定義了這種約束規範,至於如何實現這種規範,介面定義者本身並不關心.如何實現是介面實現者必須關心的,定義者和實現者兩者是解耦的.

從這點來看,介面就像是現實生活中的領導下達命令給下屬,下屬負責實現目標.如何實現目標,領導並不關心,正所謂條條大路通羅馬,手底下的人自然是八仙過海各顯神通.

go-oop-interface-type-all-roads-lead-to-rome.jpeg

領導關心結果,下屬關心實現

作為領導負責制定各種戰略目標,總攬全域性關心結果,作為下屬負責添磚加瓦實現具體細節關心過程,這種職責分離的模式就是程式語言中介面定義者和介面實現者的關係,一方負責定義行為約束,另一方負責實現這種行為規範.

如果站在領導者的角度上看問題,自然是希望下屬規規矩矩按時完成自己佈置的任務,千萬不要出現任何差池,為此甚至會出臺一系列的行為準則,簽到打卡等形式依次樹立領導威望來換取下屬的恪盡職責.

為了達到這個目標,領導者首先要在下屬中樹立足夠高的威信,做到人人信服自己,這樣手底下的人才能和自己統一戰線一致對外,團結在一起好做事.否則的話,不滿嫉妒等負面情緒就會在團隊中蔓延,逐漸侵蝕削弱團隊戰鬥力,不攻自破.

go-oop-interface-type-team-cooperation.jpeg

一般而言,這種威信的樹立要麼靠的是能力上技高一籌實力碾壓,要麼是知人善任天下賢才皆為我所用,還可以狐假虎威綠葉襯紅花思想上奴役統治.

不管是什麼方式,領導者在這場遊戲中佔據絕對領導地位,只要上層介面發號施令,下層實現都要隨之更改.如果你是領導,相信你也會喜歡這種形式的,畢竟誰心裡沒有控制慾,更何況是絕對的權力!

如果站在下層實現者的角度思考問題,顯然在這場上下級關係中實現者扮演弱勢角色,長期忍受不公平的待遇要麼崩潰,要麼揭竿而起!

Go 語言對於介面的定義者和介面的實現者的關係處理問題上,選擇了揭竿而起,實現了不同於其他傳統程式設計規範的另外一種風格規範.

這種規範常被視為是鴨子型別 duck typing --- "當看到一隻鳥走起來像鴨子,游泳起來像鴨子,叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子."

在這種規範中並不關心結構體物件是什麼型別或者說到底是不是鴨子,唯一關心的只是行為.只要滿足特定行為的結構體型別就是鴨子型別,哪怕這種鴨子可能只是一種玩具也行!所以,在這種介面定義者和實現者的關係中,實現者可以不必向介面特意宣告實現,只要最終行為上確實實現了介面中定義的行為規範,那麼就稱為該結構體實現了介面.

如果僅僅考慮介面定義者和實現者的關係,基於這種關係很容易進行下一步推斷,要麼實現者一定要宣告實現介面,隨時向領導彙報工作進度,要麼一定不宣告介面,只要保證最終能夠完成任務即可.除此之外,很明顯還存在另外一種可能性,那就是實現者可以選擇報告工作也可以選擇不報告.

那麼,這種似是而非的關係是否有存在的意義呢,又該如何表示呢以及有沒有現成程式語言基於此思路實現呢?

按照基本語義進行理解推測: 實現者需要報告給介面的方法一定是萬分緊急十分重要的規範,正所謂大是大非面前不能有任何個人情感,一旦實現者無法實現,那麼便不可饒恕,零容忍!

如果實現者不報告給介面,則表示這種規範是可選規範,如果滿足的話,自然是好的.如果有特殊情況一時沒能實現也不算是致命的問題,這類規範是可選規範,屬於錦上添花的操作.

所以要描述這種可有可無的介面定義者和實現者的關係,顯而易見的是,理應由介面定義者來指明介面的優先順序,不能由實現者定義.否則的話,你認為愛國是必選的,他認為是可選的,那麼介面的存在還有什麼意義?既然如此,介面方法在宣告時就應該宣告該介面方法是必選的還是可選的,這樣實現者實現該介面時才能有理可循,對於必選實現的介面只要沒實現就不算是真正的介面實現者,而可選的介面允許實現者可以暫時不實現.

由於個人知識經驗所限,暫不可知有沒有現成的程式語言支援這種妥協狀態,介面方法既可以宣告必選的也可以宣告可選的.個人覺得這種方式還是比較友好的,還是有存在的價值的.

如果你知道有什麼程式語言剛好是這種思路實現了介面規範,還望不吝賜教,可以留言評論相互學習下.

理論指導實踐,實踐中出真知

雖然猜測中的第三種規範是介於必須上報和必須不上報之間的妥協狀態,但是由於介面宣告時有可選和必選之分,這種區分需要有介面定義者進行指定,因此在介面和實現者的關係中還是介面定義者佔據主導地位.

當介面定義者佔據主導地位時,現成的最佳程式設計實踐告訴我們先定義介面再寫實現類,也就是先有規範再寫實現,所以實際程式設計中給我們的指導就是先抽象出共同行為,定義出介面規範,再去寫不同的實現類去實現該介面,當使用介面時就可以不區分具體的實現類直接呼叫介面本身了.

如果有一句話來描述這種行為的話,那就是理論指導實踐,先寫介面再寫實現.

同樣的,我們還知道另外一句話,這就是實踐出真知,這種思路剛好也是比較符合現實的,先寫所謂的實現類,當這種實現類寫的比較多的時候,就如繼承那樣,自然會發現彼此之間的關聯性,再抽象成介面也是水到渠成的事情,不必在程式設計剛開始就費時費力去抽象定義介面等高階功能特性.

通過上篇文章關於 Go 語言的介面的設計思想我們知道 Go 語言採用的就是後一種: 實踐中出真知. 介面實現者對於介面的實現是隱式的,也就是說某一種結構體很有可能有意無意實現了某種介面,真的是有心插花花不開,無心插柳柳成蔭.

go-oop-interface-type-miracle-by-chance.jpeg

應如何區分有沒有無心插柳

Go 語言這種似是而非若有還無的朦朧曖昧既給我們帶來了方便,同時也給我們留下了些許煩惱,假如需要知道結構體型別到底是不是介面的實現者時,反而有些費事了.

值得慶幸的是,現代 IDE 一般都比較智慧,這種介面語法雖然比較靈活但還是有規律可尋的,所以一般 IDE 也是可以智慧推測出介面和實現的關係的,並不用我們肉眼去仔細辨別.

go-oop-interface-type-ide-instruction.png

Programmer 介面的左側有個向下的箭頭,而 GoProgrammer 結構體型別左側有個向上箭頭.此時滑鼠點選箭頭可以相互跳轉,這就是 IDE 提供的視覺化效果.

如果真的需要在程式中辨別介面和實現類的關係,那麼只能藉助系統級別的方法來判斷了,準備環境如下:

首先先定義程式設計師的第一課 Hello World 的介面:

type Programmer interface {
    WriteHelloWord() string
}

然後按照不同的程式語言實現該介面,為了更加通用性表示 WriteHelloWord 的輸出結果,這裡將輸出結果 string 定義成別名形式以此表示輸出的是程式碼 Code.

type Code string

按照 Code 別名重新整理介面定義,如下:

type Programmer interface {
    WriteHelloWord() Code
}

接下來我們用 Go 語言寫第一個程式,而 Go 實現介面的方式是隱式的,並不需要關鍵字強制宣告.

type GoProgrammer struct {
}

func (g *GoProgrammer) WriteHelloWord() Code {
    return "fmt.Println(\"Hello World!\")"
}

然後,選擇 Java 程式設計師作為對比,其他物件導向程式語言類似,這裡不再贅述.

type JavaProgrammer struct {
}

func (j *JavaProgrammer) WriteHelloWord() Code {
    return "System.out.Println(\"Hello World!\")"
}

當使用者需要程式設計師寫 WriteHelloWord 程式時,此時 Go 程式設計師和 Java 程式設計師準備各顯身手,比較簡單,這裡重點是看一下介面變數的型別和值.

func writeFirstProgram(p Programmer) {
    fmt.Printf("%[1]T %[1]v %v\n", p, p.WriteHelloWord())
}

按照介面的語義,我們可以將 Go 程式設計師和 Java 程式設計師全部扔給 writeFirstProgram 方法中,此時介面的型別是具體實現類的型別,介面的值也是實現類的資料.

當然,不論是 Go 還是 Java 都可以寫出 WriteHelloWord .

func TestPolymorphism(t *testing.T) {
    gp := new(GoProgrammer)
    jp := new(JavaProgrammer)

    // *polymorphism.GoProgrammer &{} fmt.Println("Hello World!")
    writeFirstProgram(gp)
    // *polymorphism.JavaProgrammer &{} System.out.Println("Hello World!")
    writeFirstProgram(jp)
}

上述例子很簡單,我們自然也是可以一眼看出介面和實現類的關係,並且 IDE 也為我們提供非常直觀的效果,在比較複雜的結構體中這種視覺化效果尤為重要.

go-oop-interface-type-programer.png

如果你非要和我較真,說你正在用的 IDE 無法視覺化直接看出某個型別是否滿足某介面,又該怎麼辦?

我的建議是,那就換成和我一樣的 IDE 不就好了嗎!

哈哈,這只不過是我的一廂情願罷了,有些人是不願意改變的,不會隨隨便便就換一個 IDE,那我就告訴你另外一個方法來檢測型別和介面的關係.

趙本山說,沒事你就走兩步?

go-oop-interface-type-try-to-go-walk.jpg

真的是博大精深,言簡意賅!如果某個結構體型別滿足特定介面,那麼這個這個結構體的例項化後一定可以賦值給介面型別,如果不能則說明肯定沒有實現!肉眼看不出的關係,那就拿放大鏡看,編譯錯誤則不符合,編譯通過則滿足.

為了對比效果,這裡再定義一個新的介面 MyProgrammer ,除了名稱外,介面暫時和 Programmer 完全一樣.

go-oop-interface-type-myProgrammer-pass.png

IDE 並沒有報錯,左側的視覺化效果也表明 MyProgrammerProgrammer 雖然名稱不同,但是介面方法卻一模一樣,GoProgrammer 型別不僅實現了原來的 Programmer 介面還順便實現了 MyProgrammer.

不僅 GoProgrammer 是這樣,JavaProgrammer 也是如此,有意無意實現了新的介面,這也就是 Go 的介面設計不同於傳統宣告式介面設計的地方.

go-oop-interface-type-myProgrammer-goProgrammer.png

現在我們改變一下 MyProgrammer 介面中的 WriteHelloWord 方法,返回型別由別名 Code 更改成原型別 string,再試一下實際效果如何.

由於 Go 是強型別語言,即使是別名和原型別也不是相同的,正如型別之間的轉換都是強制的,沒有隱式型別轉換那樣.

因此,可以預測的是,WriteHelloWord 介面方法前後不一致,是沒有型別結構體滿足新的介面方法的,此時編譯器應該會報錯.

go-oop-interface-type-myProgrammer-fail.png

事實勝於雄辯,無論是 GoProgrammer 還是 JavaProgrammer 都沒有實現 MyProgrammer ,因此是不能賦值給型別 MyProgrammer ,編譯器確實報錯了!

並不是所有長得像的都是兄弟,也不是長得不像的就不是兄弟.

type Equaler interface {
    Equal(Equaler) bool
}

Equaler 介面定義了 Equal 方法,不同於傳統的多型,Go 的型別檢查更為嚴格,並不支援多型特性.

type T int

func (t T) Equal(u T) bool { return t == u }

如果單單看 Equal(u T) bool 方法宣告,放到其他主流的程式語言中這種情況可能是正確的,但是多型特性並不適合 Go 語言.

go-oop-interface-type-equal-fail.png

不僅僅 IDE 沒有左側視覺化的箭頭效果,硬生生的將型別宣告成介面型別也會報錯,說明的確沒有實現介面.

透過現象看本質,T.Equal 的引數型別是T ,而不是字面上所需的型別Equaler,所以並沒有實現 Equaler 介面中規定的 Equal 方法.

是不是很意外?

go-oop-interface-type-surprise.png

如果你已經看到了這裡,相信你現在不僅基本理解了物件導向的三大特性,還知道了 GO 設計的是多麼與眾不同!

這種與眾不同之處,不僅僅體現在物件導向中的型別和介面中,最基礎的語法細節上無一不體現出設計者的匠心獨運,正是這種創新也促進我們重新思考物件導向的本質,真的需要循規蹈矩按照現有的思路去設計新語言嗎?

Go 語言的語法精簡,設計簡單優雅,拋棄了某些看起來比較高階但實際使用過程中可能會比較令人困惑的部分,對於這部分的捨棄,確實在一定程度上簡化了整體的設計.

但是另一方面,如果仍然需要這種被丟棄的程式設計習慣時,只能由開發者手動實現,從這點看就不太方便了,所以只能儘可能靠近設計者的意圖,寫出真正的 Go 程式.

控制權的轉移意味著開發者承擔了更多的責任,比如型別轉換中沒有顯式型別轉換和隱式型別轉換之分,Go 僅僅支援顯式型別轉換,不會自動幫你進行隱式轉換,也沒有為了兼顧隱式型別的轉換而引入的基本型別的包裝型別,也就沒有自動拆箱和自動裝箱等複雜概念.

所以如果要實現 Equal 介面方法,那麼就應該開發者自己保證嚴格實現,這裡只需要稍微修改下就能真正實現該方法.

type T2 int

func (t T2) Equal(u Equaler) bool { return t == u.(T2) }

Equal(Equaler) bool 介面方法中的引數中要求 Equaler 介面,因此 Equal(u Equaler) bool 方法才是真正實現了介面方法.

go-oop-interface-type-equal-pass.png

只有方法名稱和簽名完全一致才是實現了介面,否則看似實現實則是其他程式語言的邏輯,放到Go 語言中並沒有實現介面.

如何保證實現者是特定型別

但是不知道你是否發現,這種形式實現的介面方法和我們熟悉的面向介面程式設計還是有所不同,任何滿足介面 Equaler 方法的型別都可以被傳入到 T2.Equal 的引數,而我們的編譯器卻不會在編譯時給出提示.

type T3 int

func (t T3) Equal(u Equaler) bool { return t == u.(T3) }

仿造 T2 實現 T3 型別,同樣也實現了 Equaler 介面所要求的 Equal 方法.

T2T3 明顯是不同的型別,編譯期間 T3 是可以傳給 T2 的,反之亦然, T2 也可以傳給 T3 .

go-oop-interface-type-equal-error-pass.png

編譯正常而執行出錯意味著後期捕捉問題的難度加大了,個人比較習慣於編譯期間報錯而不是執行報錯,Go 語言就是編譯型語言為什麼造成了編譯期間無法捕捉錯誤而只能放到執行期間了?

go-oop-interface-type-equal-error-panic.png

由此可見,t == u.(T3) 可能會丟擲異常,異常機制也是程式語言通用的一種自我保護機制,Go 語言應該也有一套機制,後續再研究異常機制,暫時不涉及.

不過我們在這裡確實看到了 u.(T3) 判斷型別的侷限性,想要確保程式良好執行,應該研究一下介面變數到底是什麼以及如何判斷型別和介面的關係.

編譯期間的判斷關係可以通過 ide 的智慧提示也可以將型別宣告給介面看看是否編譯錯誤,但這些都是編譯期間的判斷,無法解決當前執行期間的錯誤.

func TestEqualType(t *testing.T) {
    var t2 Equaler = new(T2)
    var t3 Equaler = new(T3)

    t.Logf("%[1]T %[1]v\n",t2)
    t.Logf("%[1]T %[1]v\n",t3)
    t.Logf("%[1]T %[1]v %v\n",t2,t2.Equal(t3))
}

%T %V 列印出介面變數的型別和值,從輸出結果上看 *polymorphism.T2 0xc0000921d0,我們得知介面變數的型別其實就是實現了該介面的結構體型別,介面變數的值就是該結構體的值.

t2t3 介面變數的型別因此是不同的,執行時也就自然報錯了.

說完現象找原因: Go 語言的介面並沒有保證實現介面的型別具有多型性,僅僅是約束了統一的行為規範,t2t3 都滿足了 Equal 這種規範,所以對於介面的設計效果來說,已經達到目標了.

但是這種介面設計的理念和我們所熟悉的其他程式語言的多型性是不同的,Go 並沒有多型正如沒有繼承特性一樣.

func TestInterfaceTypeDeduce(t *testing.T) {
    var t2 Equaler = new(T2)
    var t3 Equaler = new(T3)

    t.Logf("%[1]T %[1]v %[2]T %[2]v\n",t2,t2.(*T2))
    t.Logf("%[1]T %[1]v %[2]T %[2]v\n",t3,t3.(*T3))
}

go-oop-interface-type-equal-type-deduce.png

t2.(*T2)t3.(*T3) 時,均正常工作,一旦 t2.(*T3) 則會丟擲異常,因此需要特殊處理下這種情況.

根據實驗結果得知,t2.(*T2) 的型別和值恰巧就是介面變數的型別和值,如果結構體型別不能轉換成指定介面的話,則可能丟擲異常.

因此,猜測這種形式的效果上類似於強制型別轉換,將介面變數 t2 強制轉換成結構體型別,動不動就報錯或者說必須指定介面變數和結構體型別的前提,有點像其他程式語言的斷言機制.

單獨研究一下這種斷言機制,按照 Go 語言函式設計的思想,這種可能會丟擲異常的寫法並不是設計者的問題,而是我們使用者的責任,屬於使用不當,沒有檢查能否轉換成功.

v2,ok2 := t2.(*T2)

從實際執行的結果中可以看出,介面變數 t2 經過斷言為 *T2 結構體型別後得到的變數和介面變數 t2 應該是一樣的,因為他倆的型別和值完全一樣.

當這種轉換失敗時,ok 的值是 false ,此時得到的轉換結果就是 nil .

go-oop-interface-type-type-deduce.png

老子口中的無為而治空介面

介面既然是實現規範的方式,按照以往的程式設計經驗給我們的最佳實踐,我們知道介面最好儘可能的細化,最好一個介面中只有一個介面方法,足夠細分介面即減輕了實現者的負擔也方便複雜介面的組合使用.

有意思的是,Go 的介面還可以存在沒有任何介面方法的空介面,這種特殊的介面叫做空介面,無為而治,沒有任何規範約束,這不就是老子口中的順其自然,無為而治嗎?

type EmptyInterface interface {
}

道家的思想主要靠領悟,有點哲學的味道,這一點不像理科知識那樣嚴謹,可以根據已知按照一定的邏輯推測出未知,甚至預言出超時代的新理論也不是沒有可能的.

然而,道家說一生二,二生三,三生萬物,這句話看似十分富有哲理性但是實際卻很難操作,只講了開頭和結尾,並沒有講解如何生萬物,忽略了過程,全靠個人領悟,這就很難講解了.

go-oop-interface-type-dao-empty.jpg

沒有任何介面方法的空介面和一般介面之間是什麼關係?

空介面是一,是介面中最基礎的存在,有一個介面的是二,有二就會有三,自然就會有千千萬萬的介面,從而構造出介面世界觀.

func TestEmptyInterfaceTypeDeduce(t *testing.T) {
    var _ Programmer = new(GoProgrammer)
    var _ EmptyInterface = new(GoProgrammer)
}

GoProgrammer 結構體型別不僅實現了 Programmer 介面,也實現空介面,至少編譯級別沒有報錯.

但是,Go 語言的介面實現是嚴格實現,空介面沒有介面,因此沒有任何結構體都沒有實現空介面,符合一貫的設計理念,並沒有特殊處理成預設實現空介面.

go-oop-interface-type-empty-interface-not-implement.png

所以我困惑了,一方面,結構體型別例項物件可以賦值給空介面變數,而結構體型別卻又沒法實現空介面,這不是有種自相矛盾的地方嗎?

莫非是繼承不足空介面來湊

明明沒有實現空介面卻可以賦值給空介面,難不成是為了彌補語言設計的不足?

因為 Go 語言不支援繼承,自然沒有其他程式語言中的基類概念,而實際工作中有時候確實需要一種通用的封裝結構,難道是繼承不足,介面來湊?

所以設計出空介面這種特殊情況來彌補沒有繼承特性的不足?有了空介面就有了 Go 語言中的 Object 和泛型 T ,不知道這種理解對不對?

func TestEmptyInterface(t *testing.T) {
    var _ Programmer = new(GoProgrammer)
    var _ EmptyInterface = new(GoProgrammer)
    var p EmptyInterface = new(GoProgrammer)

    v, ok := p.(GoProgrammer)
    t.Logf("%[1]T %[1]v %v\n", v, ok)
}

空介面的這種特殊性值得我們花時間去研究一下,因為任何結構體型別都可以賦值給空介面,那麼此時的介面變數斷言出結構體變數是否也有配套的特殊之處呢?

func TestEmptyInterfaceTypeDeduce(t *testing.T) {
    var gpe EmptyInterface = new(GoProgrammer)

    v, ok := gpe.(Programmer)
    t.Logf("%[1]T %[1]v %v\n", v, ok)

    v, ok = gpe.(*GoProgrammer)
    t.Logf("%[1]T %[1]v %v\n", v, ok)

    switch v := gpe.(type) {
    case int:
        t.Log("int", v)
    case string:
        t.Log("string", v)
    case Programmer:
        t.Log("Programmer", v)
    case EmptyInterface:
        t.Log("EmptyInterface", v)
    default:
        t.Log("unknown", v)
    }
}

雖然接收的時候可以接收任何型別,但是實際使用過程中必須清楚知道具體型別才能呼叫例項化物件的方法,因而這種斷言機制十分重要.

func doSomething(p interface{}) {
    if i, ok := p.(int); ok {
        fmt.Println("int", i)
        return
    }
    if s, ok := p.(string); ok {
        fmt.Println("string", s)
        return
    }
    fmt.Println("unknown type", p)
}

func TestDoSomething(t *testing.T) {
    doSomething(10)
    doSomething("10")
    doSomething(10.0)
}

當然上述 doSomething 可以採用 switch 語句進行簡化,如下:

func doSomethingBySwitch(p interface{}) {
    switch v := p.(type) {
    case int:
        fmt.Println("int", v)
    case string:
        fmt.Println("string", v)
    default:
        fmt.Println("unknown type", v)
    }
}

func TestDoSomethingBySwitch(t *testing.T) {
    doSomethingBySwitch(10)
    doSomethingBySwitch("10")
    doSomethingBySwitch(10.0)
}

不一樣的介面基本用法總結

  • 型別別名
type Code string

Code 型別是原始型別 string 的別名,但 Codestring 卻不是完全相等的,因為 Go 不存在隱式型別轉換,Go 不認為這兩種型別是一樣的.

  • 介面定義者
type Programmer interface {
    WriteHelloWord() Code
}

Programmer 介面定義了 WriteHelloWord() 的方法.

  • 介面實現者
type GoProgrammer struct {
}

func (g *GoProgrammer) WriteHelloWord() Code {
    return "fmt.Println(\"Hello World!\")"
}

Go 開發者實現了 WriteHelloWord 介面方法,而這個方法剛好是 Programmer 介面中的唯一一個介面方法,因此 GoProgrammer 也就是 Programmer 介面的實現者.

這種基於方法推斷出實現者和定義者的形式和其他主流的程式語言有很大的不同,這裡並沒有顯示宣告結構體型別需要實現什麼介面,而是說幹就幹,可能一不小心就實現了某種介面都有可能.

type JavaProgrammer struct {
}

func (j *JavaProgrammer) WriteHelloWord() Code {
    return "System.out.Println(\"Hello World!\")"
}

此時,當然是我們故意實現了 Programmer 介面,以便接下來方便演示介面的基於用法.

  • 介面的使用者
func writeFirstProgram(p Programmer) {
    fmt.Printf("%[1]T %[1]v %v\n", p, p.WriteHelloWord())
}

定義了 writeFirstProgram 的函式,接收 Programmer 介面型別的引數,而介面中定義了 WriteHelloWord 的介面方法.

所以不管是 GoProgrammer 還是 JavaProgrammer 都可以作為引數傳遞給 writeFirstProgram 函式,這就是面向介面程式設計,並不在乎具體的實現者,只關心介面方法足矣.

  • 面向介面程式設計
func TestPolymorphism(t *testing.T) {
    gp := new(GoProgrammer)
    jp := new(JavaProgrammer)

    // *polymorphism.GoProgrammer &{} fmt.Println("Hello World!")
    writeFirstProgram(gp)
    // *polymorphism.JavaProgrammer &{} System.out.Println("Hello World!")
    writeFirstProgram(jp)
}

傳遞給 writeFirstProgram 函式的引數中如果是 GoProgrammer 則實現 Go 語言版本的 Hello World!,如果是 JavaProgrammer 則是 Java 版本的 System.out.Println("Hello World!")

  • 看似鬆散實則依舊嚴格的介面實現規則
type MyProgrammer interface {
    WriteHelloWord() string
}

go-oop-interface-type-alias-not-implement.png

MyProgrammerProgrammer 中的 WriteHelloWord 介面方法只有返回值型別不一樣,雖然Code 型別是 string 型別的別名,但是 Go 依舊不認為兩者相同,所以 JavaProgrammer 不能賦值給 MyProgrammer 介面型別.

  • 介面變數肚子裡是藏了啥
type GoProgrammer struct {
    name string
}

type JavaProgrammer struct {
    name string
}

給介面實現者新增 name 屬性,其餘不做改變.

func interfaceContent(p Programmer) {
    fmt.Printf("%[1]T %[1]v\n", p)
}

func TestInterfaceContent(t *testing.T) {
    var gp Programmer = &GoProgrammer{
        name:"Go",
    }
    var jp Programmer = &JavaProgrammer{
        name:"Java",
    }

    // *polymorphism.GoProgrammer &{Go}
    interfaceContent(gp)
    // *polymorphism.JavaProgrammer &{Java}
    interfaceContent(jp)
}

輸出介面變數的型別和值,結果顯示介面變數的型別就是結構體實現者的型別,介面變數的值就是實現者的值.

func (g GoProgrammer) PrintName()  {
    fmt.Println(g.name)
}

func (j JavaProgrammer) PrintName()  {
    fmt.Println(j.name)
}

現在繼續新增結構體型別的方法,可能 PrintName 方法有意無意實現了某種介面,不過在演示專案中肯定沒有實現介面.

從實驗中我們知道介面變數的型別和值都是實現者的型別和值,那麼能否通過介面變數訪問到實現者呢?

想要完成訪問實現者的目標,首先需要知道具體實現者的型別,然後才能因地制宜訪問具體實現者的方法和屬性等.

  • 斷言判斷介面變數的實現者
func TestInterfaceTypeImplMethod(t *testing.T) {
    var gp Programmer = &GoProgrammer{
        name: "Go",
    }

    // *polymorphism.GoProgrammer &{Go}
    fmt.Printf("%[1]T %[1]v\n", gp)

    if v, ok := gp.(*GoProgrammer); ok {
        // Go
        v.PrintName()
    }else{
        fmt.Println("gp is not *GoProgrammer")
    }
}

v, ok := gp.(*GoProgrammer) 將介面變數轉換成結構體型別,如果轉換成功意味著斷言成功,則可以呼叫相應結構體型別例項物件的方法和屬性.如果斷言失敗,則不可以.

  • 空介面定義和使用
type EmptyInterface interface {

}

任何結構體型別都可以賦值給空介面,此時空介面依舊和一般介面一樣的是可以採用斷言機制確定目標結構體型別.

但這並不是最常用的操作,比較常用的做法還是用來充當類似於 Object 或者泛型的角色,空介面可以接收任何型別的引數.

func emptyInterfaceParam(p interface{}){
    fmt.Printf("%[1]T %[1]v",p)

    switch v := p.(type) {
    case int:
        fmt.Println("int", v)
    case string:
        fmt.Println("string", v)
    case Programmer:
        fmt.Println("Programmer", v)
    case EmptyInterface:
        fmt.Println("EmptyInterface", v)
    default:
        fmt.Println("unknown", v)
    }
}

func TestEmptyInterfaceParam(t *testing.T) {
    var gp Programmer = new(GoProgrammer)
    var ge EmptyInterface = new(GoProgrammer)

    // *polymorphism.GoProgrammer &{}Programmer &{}
    emptyInterfaceParam(gp)

    // *polymorphism.GoProgrammer &{}Programmer &{}
    emptyInterfaceParam(ge)
}

好了,關於 Go 語言的介面部分暫時結束了,關於物件導向程式設計風格的探索也告一段落,接下來將開始探索 Go 的一等公民函式以及函數語言程式設計.敬請期待,希望學習路上,與你同行!

go-oop-interface-type-thank_you.png

上述列表是關於 Go 語言物件導向的全部系列文章,詳情見微信公眾號「雪之夢技術驛站」,如果本文對你有所幫助,歡迎轉發分享,如有描述不當之處,請一定要留言評論告訴我,感謝~

雪之夢技術驛站

相關文章