1. 程式人生 > >go 學習筆記之無心插柳柳成蔭的介面和無為而治的空介面

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 也為我們提供非常直觀的效果,在比較複雜的結構體中這種視覺化效果尤為重要.

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

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

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

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

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

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

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

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

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

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

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

事實勝於雄辯,無論是 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 語言.

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

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

是不是很意外?

如果你已經看到了這裡,相信你現在不僅基本理解了面向物件的三大特性,還知道了 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 語言中並沒有實現介面.

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

但是不知道你是否發現,這種形式實現的介面方法和我們熟悉的面向介面程式設計還是有所不同,任何滿足介面 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 語言就是編譯型語言為什麼造成了編譯期間無法捕捉錯誤而只能放到執行期間了?

由此可見,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))
}

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

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

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

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

v2,ok2 := t2.(*T2)

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

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

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

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

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

type EmptyInterface interface {
}

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

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

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

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

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

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

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

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

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

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

因為 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
}

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是不是面嚮物件語言?能不能面向物件程式設計?
  • go 學習筆記之詳細說一說封裝是怎麼回事
  • go 學習筆記之是否支援以及如何實現繼承
  • go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計?

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

相關推薦

go 學習筆記無心介面無為而治介面

如果你還了解程式設計概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點選 go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計? ,否則的話,請自動忽略上文,繼續探索 Go 語言的介面有什麼不同之處. 如無法自動跳轉到公眾號「雪之夢技術驛站」文章,可以點選我的頭像,動動你的小手翻翻歷史文

Go學習筆記高階資料型別

高階資料型別,僅僅是做個概念認識,等到其他相關知識的學習時,再著重分析。 1 function 將 function 作為資料型別的語言有很多,函數語言程式設計的核心理念。 function 是“第一等公民”,function 與其他資料型別一樣,處於平等地位,可以賦值給

go 學習筆記初識 go 語言

Go 是一種開源程式語言,可以輕鬆構建簡單,可靠,高效的軟體. 摘錄自 github: https://github.com/golang/go,其中官網(國外): https://golang.org 和官網(國內): https://golang.google.cn/ Go 是 Google 公司

go 學習筆記環境搭建

千里之行始於足下,開始 Go 語言學習之旅前,首先要搭建好本地開發環境,然後就可以放心大膽瞎折騰了. Go 的環境安裝和其他語言安

go 學習筆記工作空間

搭建好 Go 的基本環境後,現在可以正式開始 Go 語言的學習之旅,初學時建議在預設的 GOPATH 工作空間規範編寫程式碼,基本目錄結構大概是這個樣子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.

go 學習筆記走進Goland編輯器

工欲善其事必先利其器,命令列工具雖然能夠在一定程度上滿足基本操作的需求,但實際工作中總不能一直使用命令列工具進行編碼操作吧? 學習 Go 語言同樣如此,為此需要尋找一個強大的 IDE 整合環境幫助我們快速開發,據我所知,市面上比較流行的可能有三個選擇: LiteIDE X : LiteIDE 是一款簡單,開

go 學習筆記有意思的變數不安分的常量

首先希望學習 Go 語言的愛好者至少擁有其他語言的程式設計經驗,如果是完全零基礎的小白使用者,本教程可能並不適合閱讀或嘗試閱讀看看,系列筆記的目標是站在其他語言的角度學習新的語言,理解 Go 語言,進而寫出真正的 Go 程式. 程式語言中一般都有變數和常量的概念,對於學習新語言也是一樣,變數指的是不同程式語言

go 學習筆記值得特別關注的基礎語法有哪些

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變數和不安分的常量. 相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變數和常量與其他主要的程式語言的異同,為了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變數和常量

go 學習筆記陣列還是切片都沒什麼不一樣

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的程式語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的程式語言切換到 Go 語言的程式設計習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美! 在學習 Go 的內建容器前,同樣的,

go 學習筆記go是不是面嚮物件語言是否支援面對物件程式設計?

面向物件程式設計風格深受廣大開發者喜歡,尤其是以 C++, Java 為典型代表的程式語言大行其道,十分流行! 有意思的是這兩中語言幾乎毫無意外都來源於 C 語言,卻不同於 C 的面向過程程式設計,這種面向物件的程式設計風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成為設計的標準,從而讓我們

go 學習筆記詳細說一說封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計? 雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現. 但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可

go 學習筆記是否支援以及如何實現繼承

熟悉面向物件的小夥伴們可能會知道封裝,繼承和多型是最主要的特性,為什麼前輩們會如此看重這三種特性,真的那麼重要嗎? 什麼是封裝 什麼是封裝,封裝有什麼好處以及怎麼實現封裝? 相信大多數小夥伴們都有自己的理解,簡而言之,言而簡之,封裝是遮蔽內部實現細節,僅僅對外暴露出有價值介面. 正如平時工作中使用的電

go 學習筆記萬萬沒想到寵物店竟然催生出面向介面程式設計?

到底是要貓還是要狗 在上篇文章中,我們編撰了一則簡短的小故事用於講解了什麼是面向物件的繼承特性以及 Go 語言是如何實現這種繼承語

go 學習筆記僅僅需要一個示例就能講清楚什麼閉包

本篇文章是 Go 語言學習筆記之函數語言程式設計系列文章的第二篇,上一篇介紹了函式基礎,這一篇文章重點介紹函式的重要應用之一: 閉包 空談誤國,實幹興邦,以具體程式碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例程式碼開始本文的學習吧! 斐波那契數列是形如 1 1 2 3 5

go 學習筆記10 分鐘簡要理解 go 語言閉包技術

閉包是主流程式語言中的一種通用技術,常常和函數語言程式設計進行強強聯合,本文主要是介紹 Go 語言中什麼是閉包以及怎麼理解閉包. 如果讀者對於 Go 語言的閉包還不是特別清楚的話,可以參考上一篇文章 go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包. 或者也可以直接無視,因為接下來會回顧一下前情概要,現在你

go 學習筆記解讀什麼是defer延遲函式

Go 語言中有個 defer 關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患". 延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了. 出入成雙有始有終

[C#學習筆記異步編程模式2]BeginInvokeEndInvoke方法 (轉載)

cti otf 函數返回 編程模式 catch 數值 gin 單線程 blog 為什麽要進行異步回調?眾所周知,普通方法運行,是單線程的,如果中途有大型操作(如:讀取大文件,大批量操作數據庫,網絡傳輸等),都會導致方法阻塞,表現在界面上就是,程序卡或者死掉,界面元素不動了,

學習筆記《Java核心技術卷I》---- 第六章 介面、lambda表示式與內部類

介面中的所有方法都自動地屬於public。因此,在介面中宣告方法時,不必提供關鍵字public;但是在實現介面的類中,必須在實現介面中的方法時把介面中的方法宣告為public,如果不宣告,那就預設包訪問許可權,編譯器會報錯 實現Comparabale介面,必須實現其中的compareTo

Python學習筆記遍歷目錄檔案(遞迴walk())

python中遍歷指定目錄下所有的檔案和資料夾,包含多級目錄,有兩種方法,一種是通過遞迴思想去遍歷,另一種是os模組的walk()函式 要列出目錄結構 一.遞迴方法 #coding:utf-8 import os a

Java學習筆記集合(三):ArrayList集合的原理特點

package com.collection; import java.util.ArrayList; import org.junit.Test; /* 集合的體系: ----| Colle