文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇)
對於一般的語言使用者來說 ,20% 的語言特性就能夠滿足 80% 的使用需求,剩下在使用中掌握。基於這一理論,Go 基礎系列的文章不會刻意追求面面俱到,但該有知識點都會覆蓋,目的是帶你快跑趕上 Golang 這趟新車。
最近工作上和生活上的事情都很多,這篇文章計劃是週末發的,但是週末太忙時間不夠,同時為了保證文章質量,反覆修改到現在才算完成。
有時候還是很想回到學校,一心只用讀書睡覺打遊戲的日子,成年人的世界總是被各種中斷。不過,不用擔心 lemon 能處理好,答應大家要寫完的 Go 基礎系列可能會遲到,但不會缺席。
今天我們來繼續學習,Go 中的物件導向程式設計思想,包括 方法 和 介面 兩大部分學習內容。
通過學習本文,你將瞭解:
- Go 的方法定義
- 方法和函式的區別
- 方法傳值和傳指標差異
- 什麼是介面型別
- 如何判斷介面底層值型別
- 什麼是空介面
- nil 介面 和nil 底層值
如果你使用 C++ 或 Java 這類物件導向的語言,肯定知道類 class
和方法 method
的概念,Golang 中沒有class
關鍵字,但有上節介紹的 struct
結構體提供類似功能,配合方法和介面的支援,完成物件導向的程式設計完全沒有問題,下面我們就來學習下方法和介面。
方法
定義
方法就是一類帶特殊的接收者引數的函式 ,這些特殊的引數可以是結構體也可以是結構體指標,但不能是內建型別。
為了便於說明,先來定義一個結構體 Person
包含name
和 age
屬性。
type Person struct {
name string
age int
}
下面給 Person
定義兩個方法,分別用於獲取name
和age
,重點看下程式碼中方法的定義語法。
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
修改接收者的值
上面我演示的方法 GetName
和GetAge
的接收者是Person
值,這種值傳遞方式是沒辦法修改接收者內部狀態的,比如你沒法通過方法呼叫修改 Person
的 name
或age
。
假設有個需求要修改使用者年齡,我們像下面這樣定義方法 ageWriteable
,呼叫該方法之後 p
的 name
屬性並不會變化。
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"
}
對於這個定義的方法,按下面的呼叫方式 p
和 pp
都能呼叫 GetName
方法。
怎麼做到的呢?原來 pp
在呼叫方法時 Go 默默的做了隱式的轉換,其實是按照 (*pp).GetName*()
去呼叫方法,怎麼實現轉換的這點我們不用關心,先用起來就可以。
p := Person{"lemon", 18}
pp := &Person{"lemon", 18}
fmt.Println(p.GetName(), pp.GetName()) // p 和 pp都能呼叫 GetName 方法
同理,對接收者是指標的方法,也可以按給它傳遞值的方式來呼叫,這裡不再贅述。
對方法的說明,就簡單介紹到這裡,更多細節不去深究,留給大家在使用中學習。
介面
介面我想不到準確的描述語句來說明他,通俗來講介面型別就是一類預先約定好的方法宣告集合。
介面定義就是把一系列可能實現的方法先宣告出來,後面只要哪個型別完全實現了某個介面宣告的方法,就可用這個「介面變數」來儲存這些方法的值,其實是抽象設計的概念。
可以類比 C++
中的純虛擬函式。
定義
為了說明介面如何定義,我們要做一些準備工作。
- 先來定義兩個型別,代表男人女人,他們都有屬性
name
和age
type man struct {
name string
age int
}
type woman struct {
name string
age int
}
- 再來分別定義兩個型別的方法,
getName
和getAge
用於獲取各自的姓名和年齡。
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()
方法宣告,注意介面包含的這個方法的宣告樣式,和前面我們定義的 man
與 women
的 getName
方法一致。同理 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
型別的變數 m
和 w
, 怎麼才能知道它們到底是 man
還是 women
型別呢?
有兩種方法可以確定變數 m
和 w
的底層值型別。
- 型別斷言
斷言如果不是預期的型別,就會丟擲 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
對物件導向程式設計的支援,可以看到實現的非常簡潔,並沒常用的面嚮物件語言那麼複雜的語法和關鍵字,簡單不代表不夠好,實際上也基本夠用,一句話概括就是簡潔並不簡單。
感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。
今天的技術分享就到這裡,我們下期再見。
創作不易,白票不是好習慣,如果在我這有收穫,動動手指「點贊」「關注」是對我持續創作的最大支援。
可以微信搜尋公眾號「 後端技術學堂 」回覆「資料」「1024」有我給你準備的各種程式設計學習資料。文章每週持續更新,我們下期見!