【Go語言入門系列】前面的文章:
- 【Go語言入門系列】(六)再探函式
- 【Go語言入門系列】(七)如何使用Go的方法?
- 【Go語言入門系列】(八)Go語言是不是面向物件語言?
1. 引入例子
如果你使用過Java等面向物件語言,那么肯定對介面這個概念并不陌生,簡單地來說,介面就是規范,如果你的類實作了介面,那么該類就必須具有介面所要求的一切功能、行為,介面中通常定義的都是方法,
就像玩具工廠要生產玩具,生產前肯定要先拿到一個生產規范,該規范要求了玩具的顏色、尺寸和功能,工人就按照這個規范來生產玩具,如果有一項要求沒完成,那就是不合格的玩具,
如果你之前還沒用過面向物件語言,那也沒關系,因為Go的介面和Java的介面有區別,直接看下面一個實體代碼,來感受什么是Go的介面,后面也圍繞該例代碼來介紹,
package main
import "fmt"
type people struct {
name string
age int
}
type student struct {
people //"繼承"people
subject string
school string
}
type programmer struct {
people //"繼承"people
language string
company string
}
type human interface { //定義human介面
say()
eat()
}
type adult interface { //定義adult介面
say()
eat()
drink()
work()
}
type teenager interface { //定義teenager介面
say()
eat()
learn()
}
func (p people) say() { //people實作say()方法
fmt.Printf("我是%s,今年%d,\n", p.name, p.age)
}
func (p people) eat() { //people實作eat()方法
fmt.Printf("我是%s,在吃飯,\n", p.name)
}
func (s student) learn() { //student實作learn()方法
fmt.Printf("我在%s學習%s,\n", s.school, s.subject)
}
func (s student) eat() { //student重寫eat()方法
fmt.Printf("我是%s,在%s學校食堂吃飯,\n", s.name, s.school)
}
func (pr programmer) work() { //programmer實作work()方法
fmt.Printf("我在%s用%s作業,\n", pr.company, pr.language)
}
func (pr programmer) drink() {//programmer實作drink()方法
fmt.Printf("我是成年人了,能大口喝酒,\n")
}
func (pr programmer) eat() { //programmer重寫eat()方法
fmt.Printf("我是%s,在%s公司餐廳吃飯,\n", pr.name, pr.company)
}
func main() {
xiaoguan := people{"行小觀", 20}
zhangsan := student{people{"張三", 20}, "數學", "銀河大學"}
lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}
var h human
h = xiaoguan
h.say()
h.eat()
fmt.Println("------------")
var a adult
a = lisi
a.say()
a.eat()
a.work()
fmt.Println("------------")
var t teenager
t = zhangsan
t.say()
t.eat()
t.learn()
}
運行:
我是行小觀,今年20,
我是行小觀,在吃飯,
------------
我是李四,今年21,
我是李四,在火星有限公司公司餐廳吃飯,
我在火星有限公司用Go作業,
------------
我是張三,今年20,
我是張三,在銀河大學學校食堂吃飯,
我在銀河大學學習數學,
這段代碼比較長,你可以直接復制粘貼運行一下,下面好好地解釋一下,
2. 介面的宣告
上例中,我們宣告了三個介面human、adult、teenager:
type human interface { //定義human介面
say()
eat()
}
type adult interface { //定義adult介面
say()
eat()
drink()
work()
}
type teenager interface { //定義teenager介面
say()
eat()
learn()
}
例子擺在這里了,可以很容易總結出它的特點,
- 介面
interface和結構體strcut的宣告類似:
type interface_name interface {
}
- 介面內部定義了一組方法的簽名,何為方法的簽名?即方法的方法名、引數串列、回傳值串列(沒有接收者),
type interface_name interface {
方法簽名1
方法簽名2
...
}
3. 如何實作介面?
先說一下上例代碼的具體內容,
有三個介面分別是:
-
human介面:有say()、eat()方法簽名, -
adult介面:有say()、eat()、drink()、work()方法簽名, -
teenager介面:有say()、eat()、learn()方法簽名,
有三個結構體分別是:
people結構體:有say()、eat()方法,student結構體:有匿名欄位people,所以可以說student“繼承”了people,有learn()方法,并“重寫”了eat()方法,programmer結構體:有匿名欄位people,所以可以說programmer“繼承”了people,有work()、drink()方法,并“重寫”了eat()方法,
前面說過,介面就是規范,要想實作介面就必須遵守并具備介面所要求的一切,現在好好看看上面三個結構體和三個介面之間的關系:
people結構體有human介面要求的say()、eat()方法,
student結構體有teenager介面要求的say()、eat()、learn()方法,
programmer結構體有adult介面要求的say()、eat()、drink()、work()方法,
雖然student和programmer都重寫了say()方法,即內部實作和接收者不同,但這沒關系,因為介面中只是一組方法簽名(不管內部實作和接收者),
所以我們現在可以說:people實作了human介面,student實作了human、teenager介面,programmer實作了human、adult介面,
是不是感覺很巧妙?不需要像Java一樣使用implements關鍵字來顯式地實作介面,只要型別實作了介面中定義的所有方法簽名,就可以說該型別實作了該介面,(前面都是用結構體舉例,結構體就是一個型別),
換句話說:介面負責指定一個型別應該具有的方法,該型別負責決定這些方法如何實作,
在Go中,實作介面可以這樣理解:programmer說話像adult、吃飯像adult、喝酒像adult、作業像adult,所以programmer是adult,
4. 介面值
介面也是值,這就意味著介面能像值一樣進行傳遞,并可以作為函式的引數和回傳值,
4.1. 介面變數存值
func main() {
xiaoguan := people{"行小觀", 20}
zhangsan := student{people{"張三", 20}, "數學", "銀河大學"}
lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}
var h human //定義human型別變數
h = xiaoguan
var a adult //定義adult型別變數
a = lisi
var t teenager //定義teenager型別變數
t = zhangsan
}
如果定義了一個介面型別變數,那么該變數中可以存盤實作了該介面的任意型別值:
func main() {
//這三個人都實作了human介面
xiaoguan := people{"行小觀", 20}
zhangsan := student{people{"張三", 20}, "數學", "銀河大學"}
lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}
var h human //定義human型別變數
//所以h變數可以存這三個人
h = xiaoguan
h = zhangsan
h = lisi
}
不能存盤未實作該interface介面的型別值:
func main() {
xiaoguan := people{"行小觀", 20} //實作human介面
zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} //實作teenager介面
lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} //實作adult介面
var a adult //定義adult型別變數
//但zhangsan沒實作adult介面
a = zhangsan //所以a不能存zhangsan,會報錯
}
否則會類似這樣報錯:
cannot use zhangsan (type student) as type adult in assignment:
student does not implement adult (missing drink method)
也可以定義介面型別切片:
func main() {
var sli = make([]human, 3)
sli[0] = xiaoguan
sli[1] = zhangsan
sli[2] = lisi
for _, v := range sli {
v.say()
}
}
4.2. 空介面
所謂空介面,即定義了零個方法簽名的介面,
空介面可以用來保存任何型別的值,因為空介面中定義了零個方法簽名,這就相當于每個型別都會實作實作空介面,
空介面長這樣:
interface {}
下例代碼展示了空介面可以保存任何型別的值:
package main
import "fmt"
type people struct {
name string
age int
}
func main() {
xiaoguan := people{"行小觀", 20}
var ept interface{} //定義一個空介面變數
ept = 10 //可以存整數
ept = xiaoguan //可以存結構體
ept = make([]int, 3) //可以存切片
}
4.3. 介面值作為函式引數或回傳值
看下例:
package main
import "fmt"
type sayer interface {//介面
say()
}
func foo(a sayer) { //函式的引數是介面值
a.say()
}
type people struct { //結構體型別
name string
age int
}
func (p people) say() { //people實作了介面sayer
fmt.Printf("我是%s,今年%d歲,", p.name, p.age)
}
type MyInt int //MyInt型別
func (m MyInt) say() { //MyInt實作了介面sayer
fmt.Printf("我是%d,\n", m)
}
func main() {
xiaoguan := people{"行小觀", 20}
foo(xiaoguan) //結構體型別作為引數
i := MyInt(5)
foo(i) //MyInt型別作為引數
}
運行:
我是行小觀,今年20歲,
我是5,
由于people和MyInt都實作了sayer介面,所以它們都能作為foo函式的引數,
5. 型別斷言
上一小節說過,interface型別變數中可以存盤實作了該interface介面的任意型別值,
那么給你一個介面型別的變數,你怎么知道該變數中存盤的是什么型別的值呢?這時就需要使用型別斷言了,型別斷言是這樣使用的:
t := var_interface.(val_type)
var_interface:一個介面型別的變數,
val_type:該變數中存盤的值的型別,
你可能會問:我的目的就是要知道介面變數中存盤的值的型別,你這里還讓我提供值的型別?
注意:這是型別斷言,你得有個假設(猜)才行,然后去驗證猜對得對不對,
如果正確,則會回傳該值,你可以用t去接收;如果不正確,則會報panic,
話說多了容易迷糊,直接看代碼,還是用本章一開始舉的那個例子:
func main() {
zhangsan := student{people{"張三", 20}, "數學", "銀河大學"}
var x interface{} = zhangsan //x介面變數中存了一個student型別結構體
var y interface{} = "HelloWorld" //y介面變數中存了一個string型別的字串
/*現在假設你不知道x、y中存的是什么型別的值*/
//現在使用型別斷言去驗證
//a := x.(people) //報panic
//fmt.Println(a)
//panic: interface conversion: interface {} is main.student, not main.people
a := x.(student)
fmt.Println(a) //列印{{張三 20} 數學 銀河大學}
b := y.(string)
fmt.Println(b) //列印 HelloWorld
}
第一次,我們斷言x中存盤的變數是people型別,但實際上是student型別,所以報panic,
第二次,我們斷言x中存盤的變數是student型別,斷言對了,所以會把x的值賦給a,
第三次,我們斷言y中存盤的變數是string型別,也斷言對了,
有時候我們并不需要值,只想知道介面變數中是否存盤了某型別的值,型別斷言可以回傳兩個值:
t, ok := var_interface.(val_type)
ok是個布林值,如果斷言對了,為true;如果斷言錯了,為false且不報panic,但t會被置為“零值”,
//斷言錯誤
value, ok := x.(people)
fmt.Println(value, ok) //列印{ 0} false
//斷言正確
_, ok := y.(string)
fmt.Println(ok) //true
6. 型別選擇
型別斷言其實就是在猜介面變數中存盤的值的型別,
因為我們并不確定該介面變數中存盤的是什么型別的值,所以肯定會考慮足夠多的情況:當是int型別的值時,采取這種操作,當是string型別的值時,采取那種操作等,這時你可能會采用if...else...來實作:
func main() {
xiaoguan := people{"行小觀", 20}
var x interface{} = 12
if value, ok := x.(string); ok { //x的值是string型別
fmt.Printf("%s是個字串,開心", value)
} else if value, ok := x.(int); ok { //x的值是int型別
value *= 2
fmt.Printf("翻倍了,%d是個整數,哈哈", value)
} else if value, ok := x.(people); ok { //x的值是people型別
fmt.Println("這是個結構體,", value)
}
}
這樣顯得有點啰嗦,使用switch...case...會更加簡潔,
switch value := x.(type) {
case string:
fmt.Printf("%s是個字串,開心", value)
case int:
value *= 2
fmt.Printf("翻倍了,%d是個整數,哈哈", value)
case human:
fmt.Println("這是個結構體,", value)
default:
fmt.Printf("前面的case都沒猜對,x是%T型別", value)
fmt.Println("x的值為", value)
}
這就是型別選擇,看起來和普通的 switch 陳述句相似,但不同的是 case 是型別而不是值,
當介面變數x中存盤的值和某個case的型別匹配,便執行該case,如果所有case都不匹配,則執行 default,并且此時value的型別和值會和x中存盤的值相同,
7. “繼承”介面
這里的“繼承”并不是面向物件的繼承,只是借用該詞表達意思,
我們已經在【Go語言入門系列】(八)Go語言是不是面向物件語言?一文中使用結構體時已經體驗了匿名欄位(嵌入欄位)的好處,這樣可以復用許多代碼,比如欄位和方法,如果你對通過匿名欄位“繼承”得到的欄位和方法不滿意,還可以“重寫”它們,
對于介面來說,也可以通過“繼承”來復用代碼,實際上就是把一個介面當做匿名欄位嵌入另一個介面中,下面是一個實體:
package main
import "fmt"
type animal struct { //結構體animal
name string
age int
}
type dog struct { //結構體dog
animal //“繼承”animal
address string
}
type runner interface { //runner介面
run()
}
type watcher interface { //watcher介面
runner //“繼承”runner介面
watch()
}
func (a animal) run() { //animal實作runner介面
fmt.Printf("%s會跑\n", a.name)
}
func (d dog) watch() { //dog實作watcher介面
fmt.Printf("%s在%s看門\n", d.name, d.address)
}
func main() {
a := animal{"小動物", 12}
d := dog{animal{"哮天犬", 13}, "天庭"}
a.run()
d.run() //哮天犬可以呼叫“繼承”得到的介面中的方法
d.watch()
}
運行:
小動物會跑
哮天犬會跑
哮天犬在天庭看門
作者簡介
【作者】:行小觀
【公眾號】:行人觀學
【簡介】:一個面向學習的賬號,用有趣的語言寫系列文章,包括Java、Go、資料結構和演算法、計算機基礎等相關文章,
本文章屬于系列文章「Go語言入門系列」,本系列從Go語言基礎開始介紹,適合從零開始的初學者,
歡迎關注,我們一起踏上編程的行程,
如有錯誤,還請指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/343.html
標籤:Go
上一篇:分布式系統選主場景分析及實作
