若是你還了解編程概念中的接口概念,那麼我建議你最好仍是先閱讀上一篇文章.詳情請點擊 go 學習筆記之萬萬沒想到寵物店居然催生出面向接口編程? ,不然的話,請自動忽略上文,繼續探索 Go
語言的接口有什麼不一樣之處.程序員
如沒法自動跳轉到公衆號「雪之夢技術驛站」文章,能夠點擊個人頭像,動動你的小手翻翻歷史文章,相信聰明的你必定能夠找到相關文章.編程
接口是面向對象編程風格中繼封裝概念後的另外一個重要概念,封裝包含兩方面含義:數據和行爲的封裝.微信
關於封裝的概念這裏一樣再也不贅述,有興趣的話,能夠閱讀go 學習筆記之詳細說一說封裝是怎麼回事.編程語言
當現實世界中的事物或者實際需求轉移到編程世界中去實現時,這時候就須要進行建模,創建合適的模型來反映現實的事物,爲了模型的緊湊性以及更好的複用性.編程世界的前輩們總結出封裝的概念,並在此基礎上進一步衍生出一系列的編程風格,其中就包括面向對象中的繼承概念.ide
關於繼承的概念這裏一樣再也不贅述,有興趣的話,能夠閱讀go 學習筆記之是否支持以及如何實現繼承.函數式編程
封裝和繼承都是在描述同類事物模型彼此共性,正如貓和狗都是動物,運用繼承的概念表示的話,貓和狗繼承自動物.貓和狗不只具有各自特殊的屬性和行爲,還具有通常動物的屬性和行爲.函數
然而,並非只有同類事物才具備相同特徵.家禽鴨子是鴨子,玩具太空鴨也是鴨子,看似是同類事物實際卻只有某一方面的行爲相同而已,一個有生命,另外一個無生命.oop
針對這種狀況下統一共性行爲的方法也就是接口,是對同類事物或者不一樣類事物的某一方面行爲的統一抽象,知足該行爲規範的封裝對象稱之爲實現了該接口.學習
接口描述的是規範約束和實現的一種規則,接口定義了這種約束規範,至於如何實現這種規範,接口定義者自己並不關心.如何實現是接口實現者必須關心的,定義者和實現者二者是解耦的.ui
從這點來看,接口就像是現實生活中的領導下達命令給下屬,下屬負責實現目標.如何實現目標,領導並不關心,正所謂條條大路通羅馬,手底下的人天然是八仙過海各顯神通.
做爲領導負責制定各類戰略目標,總攬全局關心結果,做爲下屬負責添磚加瓦實現具體細節關心過程,這種職責分離的模式就是編程語言中接口定義者和接口實現者的關係,一方負責定義行爲約束,另外一方負責實現這種行爲規範.
若是站在領導者的角度上看問題,天然是但願下屬規規矩矩按時完成本身佈置的任務,千萬不要出現任何差池,爲此甚至會出臺一系列的行爲準則,簽到打卡等形式依次樹立領導威望來換取下屬的恪盡職責.
爲了達到這個目標,領導者首先要在下屬中樹立足夠高的威信,作到人人信服本身,這樣手底下的人才能和本身統一戰線一致對外,團結在一塊兒好作事.不然的話,不滿嫉妒等負面情緒就會在團隊中蔓延,逐漸侵蝕削弱團隊戰鬥力,不攻自破.
通常而言,這種威信的樹立要麼靠的是能力上技高一籌實力碾壓,要麼是知人善任天下賢才皆爲我所用,還能夠狐假虎威綠葉襯紅花思想上奴役統治.
無論是什麼方式,領導者在這場遊戲中佔據絕對領導地位,只要上層接口發號施令,下層實現都要隨之更改.若是你是領導,相信你也會喜歡這種形式的,畢竟誰內心沒有控制慾,更況且是絕對的權力!
若是站在下層實現者的角度思考問題,顯然在這場上下級關係中實現者扮演弱勢角色,長期忍受不公平的待遇要麼崩潰,要麼揭竿而起!
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
並無報錯,左側的可視化效果也代表 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]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
,咱們得知接口變量的類型其實就是實現了該接口的結構體類型,接口變量的值就是該結構體的值.
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]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
的別名,但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 %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
}
複製代碼
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]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
語言面向對象的所有系列文章,詳情見微信公衆號「雪之夢技術驛站」,若是本文對你有所幫助,歡迎轉發分享,若有描述不當之處,請必定要留言評論告訴我,感謝~