go 學習筆記之無心插柳柳成蔭的接口和無爲而治的空接口

如果你還了解編程概念中的接口概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點擊 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 語言面向對象的全部系列文章,詳情見微信公衆號「雪之夢技術驛站」,如果本文對你有所幫助,歡迎轉發分享,如有描述不當之處,請一定要留言評論告訴我,感謝~

雪之夢技術驛站

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章