go 學習筆記之無心插柳柳成蔭的介面和無為而治的空介面
- 2019 年 10 月 3 日
- 筆記
如果你還了解編程概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點擊 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 %vn", 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
並沒有報錯,左側的可視化效果也表明 MyProgrammer
和 Programmer
雖然名稱不同,但是介面方法卻一模一樣,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
方法.
T2
和 T3
明顯是不同的類型,編譯期間 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]vn",t2) t.Logf("%[1]T %[1]vn",t3) t.Logf("%[1]T %[1]v %vn",t2,t2.Equal(t3)) }
%T %V
列印出介面變數的類型和值,從輸出結果上看*polymorphism.T2 0xc0000921d0
,我們得知介面變數的類型其實就是實現了該介面的結構體類型,介面變數的值就是該結構體的值.
t2
和 t3
介面變數的類型因此是不同的,運行時也就自然報錯了.
說完現象找原因: Go
語言的介面並沒有保證實現介面的類型具有多態性,僅僅是約束了統一的行為規範,t2
和 t3
都滿足了 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]vn",t2,t2.(*T2)) t.Logf("%[1]T %[1]v %[2]T %[2]vn",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 %vn", v, ok) }
空介面的這種特殊性值得我們花時間去研究一下,因為任何結構體類型都可以賦值給空介面,那麼此時的介面變數斷言出結構體變數是否也有配套的特殊之處呢?
func TestEmptyInterfaceTypeDeduce(t *testing.T) { var gpe EmptyInterface = new(GoProgrammer) v, ok := gpe.(Programmer) t.Logf("%[1]T %[1]v %vn", v, ok) v, ok = gpe.(*GoProgrammer) t.Logf("%[1]T %[1]v %vn", 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
的別名,但Code
和string
卻不是完全相等的,因為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 %vn", 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 }
MyProgrammer
和Programmer
中的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]vn", 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]vn", 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
語言面向對象的全部系列文章,詳情見微信公眾號「雪之夢技術驛站」,如果本文對你有所幫助,歡迎轉發分享,如有描述不當之處,請一定要留言評論告訴我,感謝~