帶你學夠浪:Go語言基礎系列 - 面向對象程序設計:方法和接口


點擊上方藍色“後端技術學堂”關注後加個“星標

最新分享第一時間看!


正文共4273字,預計閱讀時長 10 分鐘

對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到,但該有知識點都會覆蓋,目的是帶你快跑趕上 Golang 這趟新車。

最近工作上和生活上的事情都很多,這篇文章計劃是週末發的,但是週末太忙時間不夠,同時爲了保證文章質量,反覆修改到現在纔算完成。

有時候還是很想回到學校,一心只用讀書睡覺打遊戲的日子,成年人的世界總是被各種中斷。

不過,不用擔心 lemon 能處理好,答應大家要寫完的 Go 基礎系列可能會遲到,但不會缺席。

今天我們來繼續學習Go 中的面向對象編程思想,包括 方法 和 接口 兩大部分學習內容。

通過學習本文,你將瞭解:

  • Go 的方法定義
  • 方法和函數的區別
  • 方法傳值和傳指針差異
  • 什麼是接口類型
  • 如何判斷接口底層值類型
  • 什麼是空接口
  • nil 接口 和nil 底層值

如果你使用 C++ 或 Java 這類面向對象的語言,肯定知道類 class 和方法 method 的概念,Golang 中沒有class關鍵字,但有上節介紹的 struct 結構體提供類似功能,配合方法和接口的支持,完成面向對象的程序設計完全沒有問題,下面我們就來學習下方法和接口。

方法

定義

方法就是一類帶特殊的接收者參數的函數 ,這些特殊的參數可以是結構體也可以是結構體指針,但不能是內置類型。

爲了便於說明,先來定義一個結構體 Person 包含nameage 屬性。

type Person struct {
 name string
 age  int
}

下面給 Person 定義兩個方法,分別用於獲取nameage ,重點看下代碼中方法的定義語法。

func (p Person) GetName() string {
 return p.name + "'s age is"
}

func (p Person) GetAge() int {
 return p.age
}

和函數定義的區別

看了上面的方法定義是不是覺得和函數定義有點類似,還記得函數的定義嗎?爲了喚起你的記憶,下面分別定義兩個相同功能的函數,大家可以對比一下。

func GetNameF(p Person) string {
 return p.name + "'s age is"
}

func GetNameF(p Person) int {
 return p.age
}

除了定義上的區別,還有調用上的區別。下面示例代碼演示了兩種調用方式的不同,在fmt.Println 中前面 2 個是正常函數調用,後面 2 個是方法調用,就是用點號. 和括號() 的區別。

p := Person{"lemon"18}
fmt.Println(GetNameF(p), GetNameF(p), p.GetName(), p.GetAge()) 
//輸出 lemon's age is 18 lemon's age is 18

修改接收者的值

上面我演示的方法 GetNameGetAge 的接收者是Person值,這種值傳遞方式是沒辦法修改接收者內部狀態的,比如你沒法通過方法調用修改 Personnameage

假設有個需求要修改用戶年齡,我們像下面這樣定義方法 ageWriteable ,調用該方法之後 pname 屬性並不會變化。

func (p *Person) ageWriteable() int {
 p.age += 10
 return p.age
}

那要怎麼才能實現對 p 的修改呢? 沒錯用  *Person 指針類型即可實現修改。類比 C++ 中用指針或引用來理解。

func (p *Person) ageWriteable() int {
 p.age += 10
 return p.age
}

隱式值與指針轉換

Golang 非常的聰明,爲了不讓你麻煩,它能自動識別方法的實際接收者類型(指針或值),並默默的幫你做轉換,以便「方法」能正確的工作。

還是用我們上面定義的方法舉例,先來看以「值」作爲接收者的方法調用。方便閱讀,我把前面的定義再寫一遍。

func (p Person) GetName() string {
 return p.name + "'s age is"
}

對於這個定義的方法,按下面的調用方式 ppp 都能調用 GetName  方法。

怎麼做到的呢?原來 pp 在調用方法時 Go 默默的做了隱式的轉換,其實是按照 (*pp).GetName*() 去調用方法,怎麼實現轉換的這點我們不用關心,先用起來就可以。

 p := Person{"lemon"18}
 pp := &Person{"lemon"18}
 fmt.Println(p.GetName(), pp.GetName()) // p 和 pp都能調用 GetName 方法

同理,對接收者是指針的方法,也可以按給它傳遞值的方式來調用,這裏不再贅述。

對方法的說明,就簡單介紹到這裏,更多細節不去深究,留給大家在使用中學習。

接口

接口我想不到準確的描述語句來說明他,通俗來講接口類型就是一類預先約定好的方法聲明集合。

接口定義就是把一系列可能實現的方法先聲明出來,後面只要哪個類型完全實現了某個接口聲明的方法,就可用這個「接口變量」來保存這些方法的值,其實是抽象設計的概念。

可以類比 C++ 中的純虛函數。

定義

爲了說明接口如何定義,我們要做一些準備工作。

  1. 先來定義兩個類型,代表男人女人,他們都有屬性 nameage
type man struct {
 name string
 age  int
}

type woman struct {
 name string
 age  int
}
  1. 再來分別定義兩個類型的方法, getNamegetAge 用於獲取各自的姓名和年齡。
func (m *man) getName() string {
 return m.name
}

func (m *woman) getName() string {
 return m.name
}

func (m *man) getAge() int {
 return m.age
}

func (m *woman) getAge() int {
 return m.age
}

好了, 下面我們的主角「接口」登場, 我們來實現一個通用的 humanIf 接口類型,這個接口包含了 getName() 方法聲明,注意接口包含的這個方法的聲明樣式,和前面我們定義的 manwomengetName 方法一致。同理 getAge()樣式也一致。

type humanIf interface {
    getName() string
    getAge() int
}

現在可以使用這個接口了!不管男人女人反正都是人,是人就可以用我的 humanIf 接口獲取姓名。

var m humanIf = &man{"lemon"18}
var w humanIf = &woman{"hanmeimei"19}
fmt.Println(m.getName(), w.getName()) 

接口類型

當給定一個接口值,我們如何知道他代表的底層值的具體類型呢?還是上面的例子,我們拿到了 humanIf 類型的變量  mw, 怎麼才能知道它們到底是 man 還是 women類型呢?

有兩種方法可以確定變量  mw 的底層值類型。

  • 類型斷言

斷言如果不是預期的類型,就會拋出 panic異常,程序終止。

如果斷言是符合預期的類型,會把調用者實際的底層值返回。

v0 := w.(man) // w保存的不是 man 類型,程序終止

v1 := m.(man) // m保存的符合 man 類型,v1被賦值 m 的底層值 

v, right := a.(man)  // 兩個返回值,第一個是值,第二代表是否斷言正確的布爾值
fmt.Println(v, right)
  • 類型選擇

相比類型斷言直接粗暴的讓程序終止,「類型選擇」語法更加的溫和,即使類型不符合也不會讓程序掛掉。

下面示例,v3  獲得 w  的底層類型,在後面 case 通過類型比較打印出匹配的類型。注意:type 也是關鍵字。

 
 switch v3 := w.(type) {
 case man:
  fmt.Println("it is type:man", v3)
 case women:
  fmt.Println("it is type:women", v3)
 default:
  fmt.Printf("unknow type:%T value:%v", v3, v3)
 }

空接口

空接口 interface{} 代表包含了 0 個方法的接口,試想一下每個類型都至少實現了零個方法,所以任何類型都可以給空接口類型賦值。

下面示例,用 man 值給空接口賦值。

  type nilIf interface{}
  var ap nilIf = &man{"lemon"18}

  //等價定義
  var ap interface{} = &man{"lemon"18//等價於上面一句

空接口可以接收任何類型的值,包括指針、值甚至是nil 值。

  // 接收指針
  var ap nilIf = &man{"lemon"18}
  fmt.Println("interface", ap)
  // 接收值
  var a nilIf = man{"lemon"18}
  fmt.Println("interface", a)
  // 接收nil值
  var b nilIf
  fmt.Println("interface", b)

處理nil接口調用

nil底層值不會引發異常

對 C 或 C++ 程序員來說空指針是噩夢,如果對空指針做操作,結果是不可預知的,很大概率會導致程序崩潰,程序莫名其妙掛掉,想想就令人頭禿。

Golang 中處理空指針這種情況要優雅的多,允許用空底層值調用接口,但是要修改方法定義,正確處理 nil 值避免程序崩潰。

func (m *man) getName() string {
 if m == nil {
  return "nil"
 }

 return m.name
}

下面演示了使用處理了 nil 值的方法,雖然 nilMan 是空指針,但仍然可以調用 getName 方法。

 var nilMan *man // 定義了一個空指針 nilMan
 var w humanIf = nilMan
 fmt.Println(w.getName())

nil接口引發程序異常

但是,如果接口本身是 nil 去調用方法,仍然會引發異常。

 manIf = nil
 fmt.Println("interface", manIf.getName())

總結

本節學習的接口和方法是 Golang 對面向對象程序設計的支持,可以看到實現的非常簡潔,並沒常用的面嚮對象語言那麼複雜的語法和關鍵字,簡單不代表不夠好,實際上也基本夠用,一句話概括就是簡潔並不簡單。

感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。

今天的技術分享就到這裏,我們下期再見。


創作不易,白票不是好習慣,如果有收穫,動動手指點個「在看」或給個「轉發」是對我持續創作的最大支持

另外,上次發的讀者 4 折購書福利優惠券,領取的人太多已經用完,這次又申請補充了一批,可能還有很多新關注的朋友不知道,點開本次推送次條領取。

往期精選

帶你學夠浪:Go基礎系列-環境配置和 Hello world

別再說你不懂Linux內存管理了,10張圖給你安排的明明白白!

面試都在問的微服務,一文帶你徹底搞懂!

面試官:你說對MySQL事務很熟?那我問你10個問題

資深程序員總結:分析Linux進程的6個方法,我全都告訴你

關注公衆號「後端技術學堂」

帶你一起學編程

回覆「資源」送你編程學習大禮包

包括3個G的編程「學習資源」

點個在看,一起學Golang


本文分享自微信公衆號 - 後端技術學堂(lemon10240)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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